当前位置: 首页 > ops >正文

linux初识网络及UDP简单程序

计算机网络背景

网络发展

独立模式: 计算机之间相互独立;

网络互联: 多台计算机连接在一起, 完成数据共享;

局域网LAN: 计算机数量更多了, 通过交换机和路由器连接在一起;

广域网WAN: 将远隔千里的计算机都连在一起;
所谓 "局域网" 和 "广域网" 只是一个相对的概念. 比如, 我们有 "天朝特色" 的广域网, 也可以看做一个比较大的局域网.

认识 "协议"

协议本质是一种约定,通过双方规定的方式减少沟通成本

计算机生产厂商有很多; 计算机操作系统, 也有很多; 计算机网络硬件设备, 还是有很多;

让这些不同厂商之间生产的计算机能够相互顺畅的通信就需要约定一个共同的标准, 大家都来遵守, 这就是网络协议;

协议最后的表现形式是通信双方都认识的结构体对象

发送的数据中多发的就是协议

网络协议初识

协议分层

分层:高内聚低耦合降低维护成本

分层最大的好处在于 "封装" . 面向对象例子

OSI七层模型 

OSI(Open System Interconnection,开放系统互连)七层网络模型称为开放式系统互联参考模型, 是一个逻辑上的定义和规范;

把网络从逻辑上分为了7层. 每一层都有相关、相对应的物理设备,比如路由器,交换机; OSI 七层模型是一种框架性的设计方法,其最主要的功能使就是帮助不同类型的主机实现数据传输;

它的最大优点是将服务、接口和协议这三个概念明确地区分开来,概念清楚,理论也比较完整. 通过七个层次化的结构模型使不同的系统不同的网络之间实现可靠的通讯; 但是, 它既复杂又不实用;

在实际操作的过程中,会话层、表示层是不可能接入到操作系统中的,所以在工程实践中,最终落地的是 5 层协议。

物理层我们考虑的比较少. 因此很多时候也可以称为 TCP/IP四层模型. 所以我们按照TCP/IP四层模型来讲解.

 TCP/IP五层(或四层)模型

TCP/IP是一组协议的代名词,它还包括许多协议,组成了TCP/IP协议簇. TCP/IP通讯协议采用了5层的层级结构,每一层都呼叫它的下一层所提供的网络来完成自己的需求.

物理层: 负责光/电信号的传递方式. 比如现在以太网通用的网线(双绞 线)、早期以太网采用的的同轴电缆 (现在主要用于有线电视)、光纤, 现在的wifi无线网使用电磁波等都属于物理层的概念。物理层的能力决定了最大传输速率、传输距离、抗干扰性等. 集线器(Hub)工作在物理层.

数据链路层: 负责设备之间的数据帧的传送和识别. 例如网卡设备的驱动、帧同步(就是说从网线上检测 到什么信号算作新帧的开始)、冲突检测(如果检测到冲突就自动重发)、数据差错校验等工作. 有以太 网、令牌环网, 无线LAN等标准. 交换机(Switch)工作在数据链路层.

网络层: 负责地址管理和路由选择. 例如在IP协议中, 通过IP地址来标识一台主机, 并通过路由表的方式规 划出两台主机之间的数据传输的线路(路由). 路由器(Router)工作在网路层.

传输层: 负责两台主机之间的数据传输. 如传输控制协议 (TCP), 能够确保数据可靠的从源主机发送到目标 主机.

应用层: 负责应用程序间沟通,如简单电子邮件传输(SMTP)、文件传输协议(FTP)、网络远程访问 协议(Telnet)等. 我们的网络编程主要就是针对应用层.

一般而言

对于一台主机, 它的操作系统内核实现了从传输层到物理层的内容;

对于一台路由器, 它实现了从网络层到物理层;

对于一台交换机, 它实现了从数据链路层到物理层; 对于集线器, 它只实现了物理层; 

但是并不绝对. 很多交换机也实现了网络层的转发; 很多路由器也实现了部分传输层的内容(比如端口转发);

网卡是文件,往文件里写就是往网卡里写就是向网络里写   

数据链路层在各种软件驱动中执行(网卡驱动的一部分) 

网络协议栈和OS有什么关系 

用户要访问网卡必须通过操作系统(访问硬件)但是用户不能访问操作系统内部,必须要操作系统提供他的调用接口

用户程序通过操作系统提供的API(如socket函数)来请求网络通信服务,而具体的通信实现由操作系统内核中的协议栈完成。

网络通信的本质就是贯穿协议栈的过程 

网络传输基本流程 

网络传输流程图

同一个网段内的两台主机进行文件传输.

 而跨网段的主机的文件传输. 数据从一台计算机到另一台计算机传输过程中要经过一个或多个路由器.

数据包封装和分用 

不同的协议层对数据包有不同的称谓,在传输层叫做段(segment),在网络层叫做数据报 (datagram),在链路层叫做帧(frame). 应用层数据通过协议栈发到网络上时,每层协议都要加上一个数据首部(header),称为封装 (Encapsulation).

首部信息中包含了一些类似于首部有多长, 载荷(payload)有多长, 上层协议是什么等信息. 数据封装成帧后发到传输介质上,到达目的主机后每层协议再剥掉相应的首部, 根据首部中的 "上层协议 字段" 将数据交给对应的上层协议处理.

数据封装分用过程

报头给应用层,消息(有效载荷)给用户

序号保证数据顺序,网络层报头告诉那两个主机交流

冯诺依曼体系规定,外设拿到信息交给CPU必须先给内存

网络中的地址管理 

mac地址保证局域网唯一性

IP地址保证全网唯一性

认识IP地址

IP协议有两个版本, IPv4和IPv6. 凡是提到IP协议, 没有特殊说明的, 默认都是指IPv4

IP地址是在IP协议中, 用来标识网络中不同主机的地址; 对于IPv4来说, IP地址是一个4字节, 32位的整数; 我们通常也使用 "点分十进制" 的字符串表示IP地址, 例如 192.168.0.1 ; 用点分割的每一个数字表示一个 字节, 范围是 0 - 255;

在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址.

认识MAC地址

MAC地址用来识别数据链路层中相连的节点; 长度为48位, 及6个字节. 一般用16进制数字加上冒号的形式来表示(例如: 08:00:27:03:fb:19) 在网卡出厂时就确定了, 不能修改. mac地址通常是唯一的(虚拟机中的mac地址不是真实的mac地址, 可 能会冲突; 也有些网卡支持用户配置mac地址).

局域网中所有消息在硬件上都会收到,但是会检查报文,目标地址(mac地址对比)的主机才能到用户层(报文软件),数据链路层实现,一旦判断报文不是他的就直接丢弃,上层不知道接收到过

局域网看做多台主机的共享资源

交换机隔离碰撞域,通过交换机(或类似二层设备)才能实现跨碰撞域的MAC地址通信,因为交换机是唯一能隔离和转发不同碰撞域间数据的设备。

 

网络通信宏观流程 

网络编程套接字

我们光有IP地址还不可以完成通信要有IP地址、端口号和协议类型

IP地址 ≈ 找到正确的房子

端口号 ≈ 敲对具体的门(哪个程序)

协议类型 ≈ 用对方听懂的语言交流

三者缺一不可,这正是OSI七层模型/ TCP/IP五层模型的精妙之处。

端口号

进程有很多但网络协议栈在每个操作系统内只有一套(公共资源)要有端口号能被应用层绑定并被传输层识别,这样传输层就能把数据交给进程

认识端口号

端口号(port)是传输层协议的内容.

端口号是一个2字节16位的整数; 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;

IP地址 + 端口号能够标识网络上的某一台主机的某一个进程; 一个端口号只能被一个进程占用.一个进程可以绑定多个端口号

理解 "端口号" 和 "进程ID"

我们之前在学习系统编程的时候, 学习了 pid 表示唯一一个进程;

此处我们的端口号也是唯一表示一个进程. 那么这两者之间是怎样的关系

我们在技术上也可以用pid来标识进行通信,这样是为了解耦

PID(进程ID)端口号(Port)
作用层级操作系统进程管理网络通信的传输层
唯一性全系统唯一同一主机+同一协议+同一时刻唯一
生命周期进程创建时分配,退出时释放进程绑定端口时占用,释放后可用
类比10086客服人员的工号(如:工号9527)热线电话号码(10086)

一个端口号可被多个进程轮流使用(通过accept()创建新连接)

一个进程可同时处理多个端口

另外, 一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定; (多个进程绑定一个端口号不知道交付给哪个进程(哈希))

理解源端口号和目的端口号

成功通信 = 源端口(临时ID) + 目的端口(服务ID) + 传输协议(TCP/UDP快递公司)

源端口号 = 发送方门牌号(谁寄的)
目的端口号 = 接收方门牌号(寄给谁)

假设取经团队是数据包,大唐长安和天竺是主机:

角色网络通信对应物端口号作用
唐僧数据段(TCP/UDP)携带源/目的信息
通关文牒传输层头部写明"从哪来+到哪去"
大唐驿站源端口号(随机分配)朝廷回信时知道送回哪个驿站
天竺佛经库目的端口号(固定)如来知道把经书交给谁(HTTP=80)

通信过程

  1. 唐僧(数据)从大唐驿站3000号(源端口)出发

  2. 目标明确送到天竺佛经库80号(目的端口)

  3. 如来(服务端)拿到经书后,回信地址写驿站3000号

 TCP(全双工)

传输层协议 有连接 可靠传输 面向字节流

没人访问服务器就一直等待(被动的),不能退出,必须随时随地应对客户端的请求

listen:将套接字设置为监听状态

netstat -nltp   

n能显示成数字的就显示成数字

l:listen状态

t:tcp

p:显示对应的进程

tcp面向链接,在进行通信之前要把链接建立起来

accept:成功返回整数的文件描述符,失败-1

UDP(用户数据报协议)

传输层协议 无连接 不可靠传输 面向数据报

网络字节序

内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏 移地址也有大端小端之分, 网络数据流同样有大端小端之分

如何定义网络数据流的地址:

发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;

接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;

因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.

TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.

不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;

如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;

转换函数

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络 字节序和主机字节序的转换。

这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。

例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。

如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回 ;

如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。 

 地址转换函数

本节只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址 但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换; 字符串转in_addr的函数:

in_addr转字符串的函数:

其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void *addrptr。 代码示例:

关于inet_ntoa inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是否需要调用者手动释放呢? 

man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放. 那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 参见如下代码:

运行结果如下:

因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果.

socket编程接口

socket 常见API

domain:创建套接字的域(网络通信还是本地协议)(AF_INET:IPv4)

type: 定义出的套接字类型

protocol:协议类型

返回值:依旧是文件,创建套接字的本质是创建一个文件,指向底层的网卡设备

sockaddr结构(类似于多态)

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同. 

 

前2字节的作用
• sa_family 是所有结构体的第一个字段,用于区分类型:
◦ AF_INET(IPv4):值为 2
◦ AF_UNIX(本地):值为 1
◦ AF_INET6(IPv6):值为 10
• 系统调用(如 bind()、connect())通过该字段判断如何解析后续数据。 

后续字节的差异
结构体第3字节起的内容
sockaddr_in端口号(2字节) + IPv4地址(4字节)
sockaddr_un文件路径(最多108字节)
sockaddr未定义具体格式(需转换后使用)

IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16 位端口号和32位IP地址.

IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址, 不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.

socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好 处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;

sockaddr 结构

 sockaddr_in 结构

#include <netinet/in.h>struct sockaddr_in {sa_family_t    sin_family; // 地址族(固定为 AF_INET)in_port_t      sin_port;   // 16位端口号(需用 htons() 转换字节序)struct in_addr sin_addr;   // 32位 IPv4 地址char           sin_zero[8]; // 填充字段(未使用,通常置0)
};struct in_addr {uint32_t s_addr; // 网络字节序的 IPv4 地址
};

虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主 要有三部分信息: 地址类型, 端口号, IP地址.

in_addr结构 

UDP程序 

in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数; 

忽略ip地址,只要是对应的网卡序列都能发给我,不用显示传ip地址

 netstat-naup:显示 UDP 协议相关网络连接和监听端口 的命令,同时会列出关联的进程信息。

1024前的端口号是系统内定的,一般都要有固定的应用层协议使用(http:80,https:443),sodu才能绑定

popen:帮用户创建好管道,创建子进程,将子进程执行结果通过管道交给父进程,父进程通过fp指针读到结果

在 recvfrom() 之后,client 结构体已经被内核填充了客户端的地址信息

char* inet_ntoa(struct in_addr inaddr):把4字节ip地址转换为字节返回字符串起始地址,字符串在函数内部(函数会把空间开辟好),不用手动释放,但是同步调用会有覆盖问题

可以用inet_ntop,缓冲区显示让用户传入,不用让函数维护,没有覆盖问题

聊天室 Window远程执行linux命令,Windows可以直接写套接字,做客户端,linux做服务器

sockaddr_in在htonl里

细节:传struct addr_in要强转,网络序列转主机序列,主机序列转网络序列,云服务器端口号不用绑定(0)ip地址不能直接绑定

/dev/pts/6:是一个伪终端设备文件,属于终端模拟技术的核心组成部分。

  • 本质:虚拟的终端设备,由一对主从设备(/dev/ptmx + /dev/pts/N)组成,模拟物理终端的行为。

  • 作用:为远程登录(如 SSH)、图形终端窗口(如 GNOME Terminal)或程序间通信提供交互式命令行环境。

  •  /dev/pts/6 的具体含义

    部分说明
    /dev/Linux 设备文件目录
    pts伪终端从设备(Pseudo-Terminal Slave)的命名空间
    6伪终端的编号,每个打开的终端窗口或 SSH 会话会动态分配一个唯一数字
查看所有活跃的伪终端:ls /dev/pts/

聊天室 

UdpServe.hpp

#pragma once#include <iostream>
#include <string>
#include <strings.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include <unordered_map>
#include "log.hpp"// using func_t = std::function<std::string(const std::string&)>;
typedef std::function<std::string(const std::string &, const std::string &, uint16_t)> func_t;Log lg;enum{SOCKET_ERR=1,BIND_ERR
};uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";
const int size = 1024;class UdpServer{
public:UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip):sockfd_(0), port_(port), ip_(ip),isrunning_(false){}void Init(){// 1. 创建udp socket// 2. Udp 的socket是全双工的,允许被同时读写的sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // PF_INETif(sockfd_ < 0){lg(Fatal, "socket create error, sockfd: %d", sockfd_);exit(SOCKET_ERR);}lg(Info, "socket create success, sockfd: %d", sockfd_);// 2. bind socketstruct sockaddr_in local;bzero(&local, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port_); //需要保证我的端口号是网络字节序列,因为该端口号是要给对方发送的local.sin_addr.s_addr = inet_addr(ip_.c_str()); //1. string -> uint32_t 2. uint32_t必须是网络序列的 // ??// local.sin_addr.s_addr = htonl(INADDR_ANY);if(bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) < 0){lg(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));exit(BIND_ERR);}lg(Info, "bind success, errno: %d, err string: %s", errno, strerror(errno));}void CheckUser(const struct sockaddr_in &client, const std::string clientip, uint16_t clientport){auto iter = online_user_.find(clientip);if(iter == online_user_.end()){online_user_.insert({clientip, client});std::cout << "[" << clientip << ":" << clientport << "] add to online user." << std::endl;}}void Broadcast(const std::string &info, const std::string clientip, uint16_t clientport){for(const auto &user : online_user_){std::string message = "[";message += clientip;message += ":";message += std::to_string(clientport);message += "]# ";message += info;socklen_t len = sizeof(user.second);sendto(sockfd_, message.c_str(), message.size(), 0, (struct sockaddr*)(&user.second), len);}}void Run(/*func_t func*/) // 对代码进行分层{isrunning_ = true;char inbuffer[size];while(isrunning_){memset(inbuffer,0,sizeof(inbuffer));struct sockaddr_in client;socklen_t len = sizeof(client);ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&client, &len);if(n < 0){lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));continue;}uint16_t clientport = ntohs(client.sin_port);std::string clientip = inet_ntoa(client.sin_addr);CheckUser(client, clientip, clientport);std::string info = inbuffer;Broadcast(info,clientip, clientport);}}~UdpServer(){if(sockfd_>0) close(sockfd_);}
private:int sockfd_;     // 网路文件描述符std::string ip_; // 任意地址bind 0uint16_t port_;  // 表明服务器进程的端口号bool isrunning_;std::unordered_map<std::string, struct sockaddr_in> online_user_;
};

UdpClient.cc

#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include "Terminal.hpp"using namespace std;
void Usage(string proc)
{cout<<"\n\rUsage: "<<proc<<"serveip serverport\n"<<endl;
}struct ThreadData
{struct sockaddr_in server;int sockfd;string serverip;
};void* recv_message(void* args)
{OpenTerminal();ThreadData* td = static_cast<ThreadData*>(args);char buffer[1024];while(true){memset(buffer,0,sizeof(buffer));struct sockaddr_in temp;socklen_t len = sizeof(temp);ssize_t s = recvfrom(td->sockfd,buffer,1023,0,(struct sockaddr*)&temp,&len);if(s>0 && s < sizeof(buffer)){buffer[s] = 0;cerr<<buffer<<endl;}}
}void *send_message(void *args)
{ThreadData *td = static_cast<ThreadData *>(args);string message;socklen_t len = sizeof(td->server);std::string welcome = td->serverip;welcome += " comming...";sendto(td->sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&(td->server), len);while (true){cout << "Please Enter@ ";getline(cin, message);// std::cout << message << std::endl;// 1. 数据 2. 给谁发sendto(td->sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&(td->server), len);}
}// 多线程
// ./udpclient serverip serverport
int main(int argc,char* argv[])
{if(argc != 3){Usage(argv[0]);exit(0);}string serverip = argv[1];uint16_t serverport = stoi(argv[2]);struct ThreadData td;td.server.sin_family = AF_INET;td.server.sin_port = htons(serverport);//要转成网络序列td.server.sin_addr.s_addr = inet_addr(serverip.c_str());td.sockfd = socket(AF_INET,SOCK_DGRAM,0);if(td.sockfd < 0){cout<<"socker error"<<endl;return 1;}td.serverip = serverip;pthread_t recvr,sender;pthread_create(&recvr,nullptr,recv_message,&td);pthread_create(&sender,nullptr,send_message,&td);pthread_join(recvr,nullptr);pthread_join(sender,nullptr);close(td.sockfd);//释放端口绑定,允许其他程序复用该端口return 0;
}

Terminal.hpp(令输入输出分离)

用终端分离

当调用 OpenTerminal() 时:

  1. dup2(fd, 2) 会将当前进程的 标准错误(stderr,文件描述符2) 重定向到 /dev/pts/2

  2. 重定向是进程级的
    只要进程未终止,后续所有写入 stderr(如 std::cerrperror)的输出都会发送到 /dev/pts/2,即使注释掉该函数并重新编译。

#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>std::string terminal = "/dev/pts/6";int OpenTerminal()
{int fd = open(terminal.c_str(), O_WRONLY);if(fd < 0){std::cerr<<"open terminal error"<<std::endl;return 1;}dup2(fd,2);return 0;
}

 main.cc

#include "UdpServe.hpp"
#include <memory>
#include <cstdio>
#include <vector>// "120.78.126.148" 点分十进制字符串风格的IP地址void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}// ./udpserver port
int main(int argc, char *argv[])
{if(argc != 2){Usage(argv[0]);exit(0);}uint16_t port = std::stoi(argv[1]);std::unique_ptr<UdpServer> svr(new UdpServer(port));svr->Init(/**/);svr->Run();return 0;
}

 远程执行命令

如果使用的是云服务器要开启端口号

(windows也可以)

UdpServe.hpp

#pragma once#include <iostream>
#include <string>
#include <strings.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include <unordered_map>
#include "log.hpp"using func_t = std::function<std::string(const std::string&)>;
//typedef std::function<std::string(const std::string &, const std::string &, uint16_t)> func_t;Log lg;enum{SOCKET_ERR=1,BIND_ERR
};uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";
const int size = 1024;class UdpServer{
public:UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip):sockfd_(0), port_(port), ip_(ip),isrunning_(false){}void Init(){// 1. 创建udp socket// 2. Udp 的socket是全双工的,允许被同时读写的sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // PF_INETif(sockfd_ < 0){lg(Fatal, "socket create error, sockfd: %d", sockfd_);exit(SOCKET_ERR);}lg(Info, "socket create success, sockfd: %d", sockfd_);// 2. bind socketstruct sockaddr_in local;bzero(&local, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port_); //需要保证我的端口号是网络字节序列,因为该端口号是要给对方发送的local.sin_addr.s_addr = inet_addr(ip_.c_str()); //1. string -> uint32_t 2. uint32_t必须是网络序列的 // ??// local.sin_addr.s_addr = htonl(INADDR_ANY);if(bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) < 0){lg(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));exit(BIND_ERR);}lg(Info, "bind success, errno: %d, err string: %s", errno, strerror(errno));}void CheckUser(const struct sockaddr_in &client, const std::string clientip, uint16_t clientport){auto iter = online_user_.find(clientip);if(iter == online_user_.end()){online_user_.insert({clientip, client});std::cout << "[" << clientip << ":" << clientport << "] add to online user." << std::endl;}}void Broadcast(const std::string &info, const std::string clientip, uint16_t clientport){for(const auto &user : online_user_){std::string message = "[";message += clientip;message += ":";message += std::to_string(clientport);message += "]# ";message += info;socklen_t len = sizeof(user.second);sendto(sockfd_, message.c_str(), message.size(), 0, (struct sockaddr*)(&user.second), len);}}void Run(func_t func) // 对代码进行分层{isrunning_ = true;char inbuffer[size];while(isrunning_){memset(inbuffer,0,sizeof(inbuffer));struct sockaddr_in client;socklen_t len = sizeof(client);ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&client, &len);if(n < 0){lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));continue;}inbuffer[n] = 0;std::string info = inbuffer;std::string echo_string = func(info);sendto(sockfd_,echo_string.c_str(),echo_string.size(),0,(const sockaddr*)&client,len);// uint16_t clientport = ntohs(client.sin_port);// std::string clientip = inet_ntoa(client.sin_addr);// CheckUser(client, clientip, clientport);// std::string info = inbuffer;// Broadcast(info,clientip, clientport);}}~UdpServer(){if(sockfd_>0) close(sockfd_);}
private:int sockfd_;     // 网路文件描述符std::string ip_; // 任意地址bind 0uint16_t port_;  // 表明服务器进程的端口号bool isrunning_;std::unordered_map<std::string, struct sockaddr_in> online_user_;
};

main.cc

#include "UdpServe.hpp"
#include <memory>
#include <cstdio>
#include <vector>// "120.78.126.148" 点分十进制字符串风格的IP地址void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}std::string Handler(const std::string &info, const std::string &clientip, uint16_t clientport)
{std::cout << "[" << clientip << ":" << clientport << "]# " << info << std::endl;std::string res = "Server get a message: ";res += info;std::cout << res << std::endl;// pid_t id = fork();// if(id == 0)// {//     // ls -a -l -> "ls" "-a" "-l"//     // exec*();// }return res;
}bool SafeCheck(const std::string &cmd)
{int safe = false;std::vector<std::string> key_word = {"rm","mv","cp","kill","sudo","unlink","uninstall","yum","top","while"};for(auto &word : key_word){auto pos = cmd.find(word);if(pos != std::string::npos) return false;}return true;
}std::string ExcuteCommand(const std::string &cmd)
{std::cout << "get a request cmd: " << cmd << std::endl;if(!SafeCheck(cmd)) return "Bad man";FILE *fp = popen(cmd.c_str(), "r");if(nullptr == fp){perror("popen");return "error";}std::string result;char buffer[4096];while(true){char *ok = fgets(buffer, sizeof(buffer), fp);if(ok == nullptr) break;result += buffer;}pclose(fp);return result;
}// ./udpserver port
int main(int argc, char *argv[])
{if(argc != 2){Usage(argv[0]);exit(0);}uint16_t port = std::stoi(argv[1]);std::unique_ptr<UdpServer> svr(new UdpServer(port));svr->Init(/**/);svr->Run(ExcuteCommand);return 0;
}

Windows client

#include <iostream>
#include <cstdio>
#include <thread>
#include <string>
#include <cstdlib>
#include <WinSock2.h>
#include <Windows.h>#pragma warning(disable : 4996)#pragma comment(lib, "ws2_32.lib")std::string serverip = "";  // 填写你的云服务器ip
uint16_t serverport = 8888; // 填写你的云服务开放的端口号int main()
{WSADATA wsd;WSAStartup(MAKEWORD(2, 2), &wsd);struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport); //?server.sin_addr.s_addr = inet_addr(serverip.c_str());SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd == SOCKET_ERROR){std::cout << "socker error" << std::endl;return 1;}std::string message;char buffer[1024];while (true){std::cout << "Please Enter@ ";std::getline(std::cin, message);if(message.empty()) continue;sendto(sockfd, message.c_str(), (int)message.size(), 0, (struct sockaddr
*)&server, sizeof(server));struct sockaddr_in temp;int len = sizeof(temp);int s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr *)&temp, &len);if (s > 0){buffer[s] = 0;std::cout << buffer << std::endl;}}closesocket(sockfd);WSACleanup();return 0;
}

 Windows中主要函数解释:

1. WSAStartup(MAKEWORD(2, 2), &wsd)

  • 作用:初始化Windows Sockets库(Winsock)。

  • 参数

    • MAKEWORD(2, 2):指定使用Winsock 2.2版本。

    • &wsd:接收初始化信息的结构体指针。

  • 返回值:成功返回0,失败返回错误码。

  • 注意:在Windows平台使用Socket前必须调用。

2. socket(AF_INET, SOCK_DGRAM, 0)

  • 作用:创建一个UDP套接字。

  • 参数

    • AF_INET:使用IPv4协议。

    • SOCK_DGRAM:指定为数据报(UDP)套接字。

    • 0:协议类型(通常为0,表示自动选择)。

  • 返回值:成功返回套接字描述符(SOCKET类型),失败返回INVALID_SOCKET

3. htons(serverport)

  • 作用:将16位的主机字节序端口号转换为网络字节序(大端序)。

  • 参数:主机字节序的端口号(如8888)。

  • 返回值:网络字节序的端口号。

4. inet_addr(serverip.c_str())

  • 作用:将点分十进制的IPv4地址(如"192.168.1.1")转换为32位网络字节序整数。

  • 参数:IPv4地址字符串。

  • 返回值:网络字节序的IP地址(in_addr_t类型),失败返回INADDR_NONE

5. sendto(sockfd, message.c_str(), (int)message.size(), 0, (struct sockaddr *)&server, sizeof(server))

  • 作用:通过UDP套接字发送数据到指定服务器。

  • 参数

    • sockfd:套接字描述符。

    • message.c_str():待发送数据的缓冲区。

    • message.size():数据长度。

    • 0:标志位(通常为0)。

    • (struct sockaddr *)&server:目标服务器地址结构体指针。

    • sizeof(server):地址结构体大小。

  • 返回值:成功返回发送的字节数,失败返回SOCKET_ERROR

6. recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr *)&temp, &len)

  • 作用:从套接字接收数据,并获取发送方的地址信息。

  • 参数

    • sockfd:套接字描述符。

    • buffer:接收数据的缓冲区。

    • 1023:缓冲区大小(预留1字节给\0)。

    • 0:标志位(通常为0)。

    • (struct sockaddr *)&temp:发送方地址结构体指针(输出参数)。

    • &len:地址结构体大小的指针(输入输出参数)。

  • 返回值:成功返回接收的字节数,失败返回SOCKET_ERROR

7. closesocket(sockfd)

  • 作用:关闭套接字,释放资源。

  • 参数:待关闭的套接字描述符。

  • 注意:Windows平台专用,Linux/Mac使用close()

8. WSACleanup()

  • 作用:清理Winsock库的资源。

  • 注意:与WSAStartup配对使用,程序退出前调用。

9. 其他辅助函数

  • std::getline(std::cin, message):从标准输入读取一行文本到message

  • buffer[s] = 0:手动添加字符串终止符\0,确保buffer是合法的C字符串。

http://www.xdnf.cn/news/15963.html

相关文章:

  • 如何给手机充电才不伤电池?
  • css3地球转动模型(动态数据)
  • 快手视觉算法面试30问全景精解
  • spring事务?
  • uniapp 报错 Not found ... at view.umd.min.js:1的问题
  • Vue3 学习教程,从入门到精通,Vue3 循环语句(`v-for`)语法知识点与案例详解(13)
  • 渗透第2次作业
  • 学习游戏制作记录(战斗系统简述以及击中效果)7.22
  • Mixed Content错误:“mixed block“ 问题
  • Kotlin 中的单例模式(Singleton)与对象声明
  • SpringBoot+Mybatis+MySQL+Vue+ElementUI前后端分离版:权限管理(三)
  • Android开发:Java与Kotlin深度对比
  • 用ffmpeg 进行视频的拼接
  • ni-app 对鸿蒙的支持现状
  • Redis的五大基本数据类型
  • 有关Spring的总结
  • 【每日算法】专题十七_多源 BFS
  • React基础(1)
  • 【HarmonyOS】ArkUI - 声明式开发范式
  • 空间曲线正交投影及其距离计算的理论与实践
  • Anaconda 路径精简后暴露 python 及工具到环境变量的配置记录 [二]
  • 苍穹外卖Day5
  • JAVA+AI教程-第三天
  • 使用Python绘制专业柱状图:Matplotlib完全指南
  • 原型与原型链
  • 三大工厂设计模式
  • 2025杭电多校赛(2)1006 半
  • I2S音频的时钟
  • Zabbix 企业级分布式监控系统深度解析
  • Leetcode力扣解题记录--第238题(前/后缀积)