有什么做论文的网站网站建设内容策划
- 作者: 五速梦信息网
- 时间: 2026年04月20日 06:53
当前位置: 首页 > news >正文
有什么做论文的网站,网站建设内容策划,页面设计最好只使用一种颜色避免视觉效果混淆,中国企业网是干什么的1.socket 1.1.什么是socket Socket 的中文翻译过来就是“套接字”。 套接字是什么#xff0c;我们先来看看它的英文含义#xff1a;插座。 Socket 就像一个电话插座#xff0c;负责连通两端的电话#xff0c;进行点对点通信#xff0c;让电话可以进行通信#xff0c;端…1.socket 1.1.什么是socket Socket 的中文翻译过来就是“套接字”。 套接字是什么我们先来看看它的英文含义插座。 Socket 就像一个电话插座负责连通两端的电话进行点对点通信让电话可以进行通信端口就像插座上的孔端口不能同时被其他进程占用。而我们建立连接就像把插头插在这个插座上创建一个 Socket 实例开始监听后这个电话插座就时刻监听着消息的传入谁拨通我这个“IP 地址和端口”我就接通谁。 事实上 Socket本身有“插座”的意思在Linux环境下用于表示进程间网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件。 换句话说Socket就是一种特殊的文件服务器和客户端各自维护一个“Socket文件”在建立连接打开后可以向文件写入内容供对方读取或者读取对方内容通讯结束时关闭文件。 在Linux操作系统中所有对象都被视作文件这使得文件描述符成为管理这些对象的核心工具。文件描述符是一个用于标识已打开文件的整数索引通过它可以进行各种I/O操作。Socket作为一种特殊类型的文件也遵循这一模式。在创建Socket时系统会为其分配一个文件描述符从而允许进程像操作文件一样读写网络数据。这种设计极大地简化了网络编程的复杂性使得开发者可以专注于应用逻辑而非底层细节。 与管道类似的Linux系统将其封装成文件的目的是为了统一接口使得读写套接字和读写文件的操作一致。 套接字和管道的区别是 管道主要应用于本地进程间通信套接字多应用于网络进程间数据的传递套接字的内核实现较为复杂不宜在学习初期深入学习。 在Linux系统中我们之前是使用PID来标识进程的用文件描述符来标识文件的。但是在socket这里就都换了。 在TCP/IP协议中 “IP地址TCP或UDP端口号”唯一标识网络通讯中的一个进程。“IP地址端口号”就对应一个socket。 欲建立连接的两个进程必须各自有一个socket来标识那么这两个socket组成的socket pair就唯一标识一个连接。刚好像插头和插座的连接一样必须两者一一对应。因此可以用Socket来描述网络连接的一对一关系。 套接字通信原理如下图所示 在网络通信中套接字一定是成对出现的。一端的发送缓冲区对应对端的接收缓冲区。我们使用同一个文件描述符来描述“发送缓冲区”和“接收缓冲区”。 实际上我们口中的Socket有多层意思一层意思是特殊的文件一层意思是表示一种特有的通信模式还有一层意思是在应用层和传输层之间的一个抽象层它把 TCP/IP 层复杂的操作抽象为几个简单的接口供应用层调用实现进程在网络中的通信。 就像下面这样子 TCP/IP协议最早在BSD UNIX上实现为TCP/IP协议设计的应用层编程接口称为socket API。 2.socket API 首先我们来看看 socket 套接字提供了下面这一批常用接口用于实现网络通信 #include sys/types.h #include sys/socket.h// 创建socket文件描述符TCP/UDP 服务器/客户端 int socket(int domain, int type, int protocol);// 绑定端口号TCP/UDP 服务器 int bind(int socket, const struct sockaddr* address, socklen_t address_len);// 开始监听socket (TCP 服务器) int listen(int socket, int backlog);// 接收连接请求 (TCP 服务器) int accept(int socket, struct sockaddr* address, socklen_t* address_len);// 建立连接 (TCP 客户端) int connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen);可以看到在这一批 API 中频繁出现了一个结构体类型 sockaddr这是个什么东西 首先我们要明白 网络通信时数据发送方、数据接收方需要明确对方的网络地址而网络的地址的三大要素就是下面这3点 协议ip端口 在socket里面这三个参数用一个结构体 sockaddr 来表示。 先说结论sockaddr是统一的接口只用一个接口完成不同套接字比如IPV4IPV6之间的通信问题。 socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket。然而,各种网络协议的地址格式并不相同。在C语言中如果直接处理就要多出重复的接口设计成统一的目的是为了设计尽量少的接口实现面向对象中的静态多态——函数重载。 2.1.sockaddr 和sockaddr_in结构体 socket 这套网络通信标准隶属于 POSIX 通信标准该标准的设计初衷就是为了实现 可移植性程序可以直接在使用该标准的不同机器中运行但有的机器使用的是网络通信有的则是使用本地通信socket 套接字为了能同时兼顾这两种通信方式提供了 sockaddr 结构体 由 sockaddr 结构体衍生出了两个不同的结构体sockaddr_in 网络套接字、sockaddr_un 域间套接字前者用于网络通信后者用于本地通信 我们今天不讨论socketaddr_un这个因为他不是用来网络通信的。 我们来看看sockaddr和sockaddr_in这两个 sockaddr在头文件#include sys/socket.h中定义sockaddr的缺陷是sa_data把目标地址和端口信息混在一起了如下 struct sockaddr { sa_family_t sin_family;//地址族char sa_data[14]; //14字节包含套接字中的目标地址和端口信息 }; sockaddr_in在头文件#includenetinet/in.h或#include arpa/inet.h中定义 该结构体解决了sockaddr的缺陷把port和addr 分开储存在两个变量中如下 struct sockaddr_in {short sin_family; // 2 字节 地址族e.g. AF_INET, AF_INET6unsigned short sin_port; // 2 字节 16位TCP/UDP 端口号 e.g. htons(3490)struct in_addr sin_addr; // 4 字节 32位IP地址char sin_zero[8]; // 8 字节 不使用 }; struct in_addr {unsigned long s_addr; // 32位IPV4地址打印的时候可以调用inet_ntoa()函数将其转换为char *类型. }; sin_port和sin_addr都必须是网络字节序NBO一般可视化的数字都是主机字节序HBO。 注释中标明了属性的含义及其字节大小这两个结构体一样大都是16个字节而且都有family属性不同的是 sockaddr用其余14个字节来表示sa_data而sockaddr_in把14个字节拆分成sin_port, sin_addr和sin_zero分别表示端口、ip地址。sin_zero用来填充字节使sockaddr_in和sockaddr保持一样大小。 我们上面提到过网络通信时数据发送方、数据接收方需要明确对方的网络地址而网络的地址的三大要素就是下面这3点 协议ip端口 大家看看sockaddr和sockaddr_in里面也没有这3样东西呢 事实上sockaddr和sockaddr_in包含的数据都是一样的但他们在使用上有区别 程序员不应操作sockaddrsockaddr是给操作系统用的程序员应使用sockaddr_in来表示地址sockaddr_in区分了地址和端口使用更方便。 此外 二者大小一样都是16个字节即占用的内存大小是一致的因此可以互相转化。二者是并列结构指向sockaddr_in结构的指针也可以指向sockaddr。 所以在网络编程中我们会对sockaddr_in结构体进行操作使用sockaddr_in来建立所需的信息先把sockaddr_in变量赋值后强制类型转换后传入用sockaddr做参数的函数。也就是说sockaddr_in用于socket定义和赋值sockaddr用于函数参数。 一般的用法为 程序员把类型、ip地址、端口填充sockaddr_in结构体然后强制转换成sockaddr作为参数传递给系统调用函数 3.UDP网络通信程序 接下来接下来实现一批基于 UDP 协议的网络程序本节只介绍基于IPv4的socket网络编程 3.1.核心功能 分别实现客户端与服务器客户端向服务器发送消息服务器收到消息后回响给客户端有点类似于 echo 指令 该程序的核心在于 使用 socket 套接字接口以 UDP 协议的方式实现简单网络通信 3.2.程序结构 程序由server.hpp server.cc client.hpp client.cc 组成大体框架如下 创建 server.hpp 服务器头文件 #pragma once#include iostreamnamespace nt_server {class UdpServer{public:// 构造UdpServer(){} // 析构~UdpServer(){} // 初始化服务器void InitServer(){}// 启动服务器void StartServer(){}private:// 字段}; }创建 server.cc 服务器源文件 #include memory // 智能指针相关头文件 #include server.hppusing namespace std; using namespace nt_server;int main() {unique_ptrUdpServer usvr(new UdpServer());//使用智能指针创建了一个UdeServer对象// 初始化服务器usvr-InitServer();// 启动服务器usvr-StartServer();return 0; }创建 client.hpp 客户端头文件 #pragma once#include iostreamnamespace nt_client {class UdpClient{public:// 构造UdpClient() {} // 析构~UdpClient() {} // 初始化客户端void InitClient() {}// 启动客户端void StartClient() {}private:// 字段}; }创建 client.cc 客户端源文件 #include memory #include client.hppusing namespace std; using namespace nt_client;int main() {unique_ptrUdpClient usvr(new UdpClient());// 初始化客户端usvr-InitClient();// 启动客户端usvr-StartClient();return 0; }为了方便后续测试再添加一个 Makefile 文件 创建 Makefile 文件 .PHONY:all all:server clientserver:server.ccg -o \( \)^ -stdc11client:client.ccg -o \( \)^ -stdc11.PHONY:clean clean:rm -rf server client准备工作完成后接下来着手填充代码内容 3.3.服务端设计 3.3.1.创建套接字——socket函数 创建套接字使用 socket 系统调用接口 socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字而socket()用于创建一个socket描述符socket descriptor它唯一标识一个socket。这个socket描述字跟文件描述符一样后续的操作都有用到它把它作为参数通过它来进行一些读写操作。 正如可以给open的传入不同参数值以打开不同的文件。创建socket的时候也可以指定不同的参数创建不同的socket描述符socket函数的三个参数分别为 domain这个是协议域协议簇决定了socket的地址类型。 常用的有下面 AF_INET用来产生IPV4 - socket 的协议使用TCP或UDP来传输用IPV4的地址AF_INET6:和上面的差不多这个是IPV6的AF_UNIX:本地协议用在Unix和Linux系统上一般都是服务端和客户端在同一台机器上时使用。 要用一个绝对路径名作为地址。 我们一般使用IPV4的AF_INET type指socket类型有面向连接的套接字SOCK_STREAM和面向消息的套接字SOCK_DGRAM。 我们看看它的参数 SOCK_STREAM这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型是用TCP协议来传输的。SOCK_DGRAM这个协议是无连接的固定长度的连接调用。该协议是不可靠的使用UDP来进行它的连接。SOCK_SEQPACKET这个协议是双线路的、可靠的连接发送固定长度的数据包进行传输。注1必须把整个包完整的接收才能够进行读取。SOCK_RAW:这个socket类型提供单一的网络访问 其中 面向连接的套接字可以理解成TCP协议数据稳定、按序传输不存在数据边界且收发数据在套接字内部有缓冲所以服务器和客户端进行I/O操作时并不会马上调用可能分多次调用面向消息的套接字可以看做UDP特点快速传输、有数据边界、数据可能丢失、传输数据大小受限。 protocol指计算机间通信中使用的协议信息。protocol一般设置为0默认协议 一般都可以为0当protocol为0时会自动选择type类型对应的默认协议。如果同一协议簇中存在多个数据传输方式相同的协议则才用第三个参数。 常用的协议有IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等。type和protocol并不是可以随意组合的如SOCK_STREAM不可以跟IPPROTO_UDP组合。 返回值 socket返回的值是一个文件描述符SOCKET类型本身也是定义为int的既然是文件描述符那么在系统中都当作是文件来对待的0,1,2分别表示标准输入、标准输出、标准错误。所以其他打开的文件描述符都会大于2, 错误时就返回 -1. 这里INVALID_SOCKET 也被定义为 -1 。 socket函数打开一个网络通讯端口如果成功的话就像open一样返回一个文件描述符应用程序可以像读写文件一样read/write在网络上收发数据。 好了socket函数学完了接下来在 server.hpp 的 InitServer() 函数中创建套接字并对创建成功/失败后的结果做打印 server.hpp #pragma once#include iostream #include cstring #include cerrno #include cstdlib #include sys/types.h #include sys/socket.hnamespace nt_server {// 错误码enum{SOCKETERR 1};class UdpServer{public:// 构造UdpServer(){} // 析构~UdpServer(){} // 初始化服务器void InitServer(){// 1.创建套接字sock socket(AF_INET, SOCKDGRAM, 0);if(sock -1)//创建失败{std::cout Create Socket Fail: strerror(errno) std::endl;exit(SOCKETERR);}// 创建成功std::cout Create Success Socket: sock std::endl;}// 启动服务器void StartServer(){}private:int sock_; // 套接字}; }因为这里是使用 UDP 协议实现的 网络通信参数1 domain 选择 AF_INET基于 IPv4 标准参数2 type 选择 SOCK_DGRAM数据报传输参数3设置为 0可以根据 SOCK_DGRAM 自动推导出使用 UDP 协议 我们运行一下 文件描述符默认 0、1、2 都已经被占用了如果再创建文件描述符会从 3 开始可以看到程序运行后创建的套接字正是 3证明套接字本质上就是文件描述符不过它用于描述网络资源 3.3.2.绑定IP地址和端口号——bind函数 bind的英文意思就是捆绑 服务端用于将把用于通信的地址和端口绑定到socket 上。所以可以猜出这个函数的参数应该包含用于通信的 socket 和服务端的 IP 地址和端口号。ip地址和端口号是放在 socketaddr_in 结构体里面的。 参数
- sockfd : 通过socket函数得到的文件描述符 - addr : 需要绑定的socket地址这个地址封装了ip和端口号的信息 - addrlen : 第二个参数结构体占的内存大小 参数1没啥好说的重点在于参数2因为我们这里是 网络通信所以使用的是 sockaddr_in 结构体要想使用该结构体还得包含下面这两个头文件 #include netinet/in.h #include arpa/inet.h 有的人可能又好奇了 这个第2个参数不是const struct sockaddr*吗?为啥要使用sockaddr_in呢 不记得的朋友去上面看看啊 我们进行网络通信的时候一般的做法是 程序员把类型、ip地址、端口填充sockaddr_in结构体然后强制转换成sockaddr作为参数传递给系统调用函数 我们需要详细了解一下这个sockaddr_in结构体 struct sockaddr_in {short sin_family; // 2 字节 地址族e.g. AF_INET, AF_INET6unsigned short sin_port; // 2 字节 16位TCP/UDP 端口号 e.g. htons(3490)struct in_addr sin_addr; // 4 字节 32位IP地址char sin_zero[8]; // 8 字节 不使用 }; struct in_addr {unsigned long s_addr; // 32位IPV4地址打印的时候可以调用inet_ntoa()函数将其转换为char 类型. }; sin_port和sin_addr都必须是网络字节序NBO一般可视化的数字都是主机字节序HBO。 我们需要一个sockaddr_in结构体再创建1个short类型一个unsigned short一个struct in_addr传进去这样子会不会很清楚 了解完 sockaddr_in 结构体中的内容后就可以创建该结构体了再定义该结构体后需要清空确保其中的字段干净可用 将变量置为 0 可用使用 bzero 函数 #include cstrins // bzero 函数的头文件struct sockaddr_in local; bzero(local, sizeof(local));获得一个干净可用的 sockaddr_in 结构体后可以正式绑定 IP 地址 和 端口号 了 server.hpp 服务器头文件 #pragma once#include iostream #include string #include cstring #include cerrno #include cstdlib #include strings.h #include sys/types.h #include sys/socket.h #include netinet/in.h #include arpa/inet.hnamespace nt_server {// 退出码enum{SOCKET_ERR 1,BIND_ERR};// 端口号默认值const uint16_t default_port 8888;class UdpServer{public:// 构造UdpServer(const std::string ip, const uint16_t port defaultport):port(port), ip(ip){} // 析构~UdpServer(){} // 初始化服务器void InitServer(){// 1.创建套接字sock socket(AF_INET, SOCKDGRAM, 0);if(sock -1){std::cout Create Socket Fail: strerror(errno) std::endl;exit(SOCKETERR);}// 创建成功std::cout Create Success Socket: sock std::endl;// 2.绑定IP地址和端口号struct sockaddr_in local;bzero(local, sizeof(local)); // 置0// 填充字段local.sin_family AF_INET; // 设置为网络通信PF_INET 也行local.sinport htons(port); // 主机序列转为网络序列local.sin_addr.s_addr inetaddr(ip.cstr()); // 点分十进制转为短整数再将主机序列转为网络序列// 绑定IP地址和端口号int n bind(sock, (const sockaddr)local, sizeof(local));if(n0){std::cout Bind IPPort Fail: strerror(errno) std::endl;exit(BINDERR);}// 绑定成功std::cout Bind IPPort Success std::endl;}// 启动服务器void StartServer(){}private:int sock; // 套接字uint16t port; // 端口号std::string ip_; // IP地址后面需要删除}; }注作为服务器需要确定自己的端口号我这里设置的是 8888这个端口号需要来回发送的这个端口号必须是网络字节序可以使用 htons 函数 有几点要说明了 端口号会在网络里互相转发需要把主机序列转换为网络序列可以使用 htons 函数需要把点分十进制的字符串转换为无符号短整数可以使用 inet_addr 函数这个函数在进行转换的同时会将主机序列转换为网络序列因为IP地址需要在网络里面发送绑定IP地址和端口号这个行为并非直接绑定到当前主机中而是在当前程序中将创建的 socket 套接字与目标IP地址与端口号进行绑定当程序终止后这个绑定关系也会随之消失 我们这里先插一个小知识 3.3.2.1.地址转换函数——字符串和struct in_addr互相转换 我们这里为什么要使用字符串来表示IP地址 首先大部分用户习惯使用的IP是点分十进制的字符串就像下面这个这样 128.11.3.31 基于IPv4的socket网络编程sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址也就是说事实上我们的IP地址就是下面的第3个成员。 struct sockaddr_in {short sin_family; // 2 字节 地址族e.g. AF_INET, AF_INET6unsigned short sin_port; // 2 字节 16位TCP/UDP 端口号 e.g. htons(3490)struct in_addr sin_addr; // 4 字节 32位IP地址char sin_zero[8]; // 8 字节 不使用 }; struct in_addr {unsigned long s_addr; // 32位IPV4地址打印的时候可以调用inet_ntoa()函数将其转换为char *类型. }; 点分十进制的IP地址不好输入我们往往先用更好输入的字符串来存储IP地址然后将字符串版的IP地址转换为struct in_addr版的IP地址也就是点分十进制版的 事实上在网络编程中经常需要进行点分十进制字符串表示的IP地址和in_addr结构体表示的IP地址之间的转换。以下是一些常用的地址转换函数它们存放在arpa/inet.h头文件中。 1. 字符串转 in_addr 结构体 有很多函数都能做到这里就举两个第一个函数就是inet_addr #include arpa/inet.hin_addr_t inet_addr(const char *cp); 该函数将点分十进制的字符串表示的IPv4地址转换为网络字节序的32位整数。返回的是in_addr_t类型通常用于填充sin_addr.s_addr字段。 示例 const char *ipString 192.168.1.1; in_addr_t ipAddress inet_addr(ipString); 第二个函数就是inet_pton #include arpa/inet.hint inet_pton(int af, const char *src, void *dst); 这个函数是更通用的函数支持IPv4和IPv6地址的转换。第一个参数 af 表示地址族常用的是 AF_INETIPv4和 AF_INET6IPv6。第二个参数 src 是输入的字符串表示的IP地址第三个参数 dst 是输出的二进制表示的IP地址。 示例 #include arpa/inet.hstruct in_addr ipv4Address; const char *ipString 192.168.1.1; inet_pton(AF_INET, ipString, (ipv4Address.s_addr)); 2. in_addr 结构体转字符串 有很多函数都能做到这里就举两个第一个函数是inet_ntoa函数 #include arpa/inet.hchar *inet_ntoa(struct in_addr in); 该函数将in_addr结构体中的IPv4地址转换为点分十进制的字符串表示。需要注意的是返回的是指向静态缓冲区的指针inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果。因此不宜多次调用不能用来多线程。 如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢?在APUE中, 明确提出inet_ntoa不是线程安全的函数; 在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问 题; 示例 #include arpa/inet.hstruct in_addr ipv4Address; ipv4Address.s_addr inet_addr(192.168.1.1); char *ipString inet_ntoa(ipv4Address); 第二个函数就是inet_ntop #include arpa/inet.hconst char *inet_ntop(int af, const void *src, char dst, socklen_t size); 这是一个更通用的函数支持IPv4和IPv6地址的转换。第一个参数 af 表示地址族常用的是 AF_INETIPv4和 AF_INET6IPv6。第二个参数 src 是输入的二进制表示的IP地址。第三个参数 dst 是输出的字符串表示的IP地址的缓冲区。第四个参数 size 是缓冲区的大小。 示例 #include arpa/inet.hstruct in_addr ipv4Address; ipv4Address.s_addr inet_addr(192.168.1.1); char ipString[INET_ADDRSTRLEN]; inet_ntop(AF_INET, (ipv4Address.s_addr), ipString, INET_ADDRSTRLEN); 这些地址转换函数是在网络编程中非常实用的工具它们使得在不同表示之间进行转换变得简单而高效。通过合理使用这些函数你可以轻松地在字符串表示和二进制表示之间转换IP地址从而更方便地进行网络编程。 server.cc 服务器源文件 #include memory // 智能指针相关头文件 #include server.hppusing namespace std; using namespace nt_server;int main() {unique_ptrUdpServer usvr(new UdpServer(8.134.110.68));// 初始化服务器usvr-InitServer();// 启动服务器usvr-StartServer();return 0; } 接下来编译并运行程序 可以发现运行错了 如果运行环境是虚拟机这个是可以运行起来的但是我们今天的运行环境是云服务器云服务器禁止绑定公网IP因为这个是虚拟化了的跟我们的机器对不上。此外一台机器可能有多张网卡有多个IP地址这样子如果只绑定了一个IP那么只能收到这个IP发来的。 所以解决方案是在绑定 IP 地址时让其选择绑定任意可用 IP 地址 这样子有两种方法 第一种方法是服务器端只需要作下面这些改动 不需要为 IP 地址而创建srring类型构造时也无需传入 IP 地址绑定 IP 地址时选择 INADDR_ANY表示绑定任何可用的 IP 地址 server.hpp 服务器头文件 class UdpServer { public:// 构造UdpServer(const uint16_t port defaultport):port(port){} // 初始化服务器 void InitServer() {// …// 填充字段local.sin_family AF_INET; // 设置为网络通信PF_INET 也行local.sinport htons(port); // 主机序列转为网络序列// local.sin_addr.s_addr inetaddr(ip.c_str()); // 点分十进制转为短整数再将主机序列转为网络序列local.sin_addr.s_addr INADDRANY; // 绑定任何可用IP地址// … }private:int sock; // 套接字uint16t port; // 端口号// std::string ip_; // 删除 };此外还有一种方法我们可以不删除这个string我们让IP绑定到0.0.0.00.0.0.0 表示任意IP地址 #pragma once //….. namespace nt_server {// 退出码enum{SOCKET_ERR 1,BIND_ERR};// 端口号默认值const uint16_t default_port 8888;const std::string0.0.0.0;//注意这里class UdpServer{public:// 构造UdpServer(const std::string ipdefaultip, const uint16_t port defaultport):port(port), ip_(ip){} // 析构~UdpServer(){} // 初始化服务器void InitServer(){//。。。。// 2.绑定IP地址和端口号struct sockaddr_in local;bzero(local, sizeof(local)); // 置0// 填充字段local.sin_family AF_INET; // 设置为网络通信PF_INET 也行local.sinport htons(port); // 主机序列转为网络序列local.sin_addr.s_addr inetaddr(ip.cstr()); // 点分十进制转为短整数再将主机序列转为网络序列//。。。。}// 启动服务器void StartServer(){}private:int sock; // 套接字uint16t port; // 端口号std::string ip_; // IP地址}; } 这样子就好了 server.cc 服务器源文件 #include memory // 智能指针相关头文件 #include server.hppusing namespace std; using namespace nt_server;int main() {unique_ptrUdpServer usvr(new UdpServer());// 初始化服务器usvr-InitServer();// 启动服务器usvr-StartServer();return 0; }再次编译并运行程序可以看到正常运行 到目前为止我们的UDP网络通信程序已经完成了最基本的环境搭建接下来就是发信息读消息那些了。 3.3.3.读取信息——recvfrom函数 读取信息使用recvfrom函数 recvfrom() 函数是一个系统调用用于从套接字接收数据。 该函数通常与无连接的数据报服务如 UDP一起使用但也可以与其他类型的套接字使用。 与简单的 recv() 函数不同recvfrom() 可以返回数据来源的地址信息。 我们来看看它的参数 1. sockfd一个已打开的套接字的描述符。 2. buf一个指针指向用于存放接收到的数据的缓冲区。 3. len缓冲区的大小以字节为单位。 4. flags控制接收行为的标志读取方式阻塞/非阻塞。通常可以设置为0但以下是一些可用的标志 MSG_WAITALL尝试接收全部请求的数据。函数可能会阻塞直到收到所有数据。MSG_PEEK查看即将接收的数据但不从套接字缓冲区中删除它【1】。其他一些标志还可以影响函数的行为但在大多数常规应用中很少使用。 前半部分主要用于读取数据并进行存放接下来看看后半部分 5. src_addr一个指针指向一个 sockaddr 结构用于保存发送数据的源地址。 6. addrlen一个值-结果参数。开始时它应该设置为 src_addr 缓冲区的大小。当 recvfrom() 返回时该值会被修改为实际地址的长度以字节为单位。 后面都是用来保存对方的地址信息的 返回值 在成功的情况下recvfrom() 返回接收到的字节数。如果没有数据可读或套接字已经关闭那么返回值为0。出错时返回 -1并设置全局变量 errno 以指示错误类型。 使用示例 struct sockaddr_in sender; socklen_t sender_len sizeof(sender); char buffer[1024];int bytes_received recvfrom(sockfd, buffer, sizeof(buffer), 0,(struct sockaddr)sender, sender_len); if (bytes_received 0) {perror(recvfrom failed);// handle error }注意 因为 recvfrom 函数的参数 src_addr 类型为 sockaddr需要将 sockaddr_in 类型强转后再进行传递 server.hpp 服务器头文件 namespace nt_server {// 退出码enum{SOCKET_ERR 1,BIND_ERR};// 端口号默认值const uint16_t default_port 8888;class UdpServer{//…..// 启动服务器void StartServer(){// 服务器是不断运行的所以需要使用一个 while(true) 死循环char buff[1024]; // 缓冲区while (true){// 1. 接收消息struct sockaddr_in peer; // 客户端结构体socklen_t len sizeof(peer); // 客户端结构体大小// 传入 sizeof(buff) - 1 表示当前传输的是字符串预留一个位置存储 \0// 传入 0 表示当前是阻塞式读取ssizet n recvfrom(sock, buff, sizeof(buff) - 1, 0, (struct sockaddr )peer, len);if (n 0)buff[n] \0;elsecontinue; // 继续读取// 2.处理数据std::string clientIp inet_ntoa(peer.sin_addr); // 获取IP地址uint16_t clientPort ntohs(peer.sin_port); // 获取端口号printf(Server get message from [%s:%d]$ %s\n, clientIp.c_str(), clientPort, buff);// 3.回响给客户端// …}}//…..}; } 到这里也算是完成一小步 3.3.4.发送消息——sendto函数 发送信息使用 sendto 函数 sendto() 函数是一个系统调用用于发送数据到一个指定的地址。 它经常与无连接的数据报协议如UDP一起使用。 不像 send() 函数只能发送数据到一个预先建立连接的远端sendto() 允许在每次发送操作时指定目的地址。 参数解释 1. sockfd一个已打开的套接字的描述符。 2. buf一个指针指向要发送的数据的缓冲区。 3. len要发送的数据的大小以字节为单位。 4. flags控制发送行为的标志也就是发送方式阻塞/非阻塞。通常可以设置为0。一些可用的标志包括 MSG_CONFIRM在数据报协议下告诉网络层该数据已经被确认。MSG_DONTROUTE不查找路由数据报将只发送到本地网络。其他标志可以影响函数的行为但在大多数常规应用中很少使用。 5. dest_addr指向 sockaddr 结构的指针该结构包含目标地址和端口信息。 6. addrlendest_addr 缓冲区的大小以字节为单位。 返回值 成功时sendto() 返回实际发送的字节数。出错时返回 -1 并设置全局变量 errno 以指示错误类型。 例子 struct sockaddr_in receiver; receiver.sin_family AF_INET; receiver.sin_port htons(12345); // Some port number inet_pton(AF_INET, 192.168.1.1, receiver.sin_addr); // Some IP addresschar message[] Hello, World!; ssize_t bytes_sent sendto(sockfd, message, sizeof(message), 0,(struct sockaddr)receiver, sizeof(receiver)); if (bytes_sent 0) {perror(sendto failed);// handle error }在这个例子中我们使用 sendto() 发送一个字符串到指定的IP地址和端口号。如果发送失败我们打印一个错误消息。 server.hpp 服务器头文件 //。。。 namespace nt_server {// 退出码enum{SOCKET_ERR 1,BIND_ERR};// 端口号默认值const uint16_t default_port 8888;class UdpServer{//…..// 启动服务器void StartServer(){// 服务器是不断运行的所以需要使用一个 while(true) 死循环char buff[1024]; // 缓冲区while (true){// 1. 接收消息struct sockaddr_in peer; // 客户端结构体socklen_t len sizeof(peer); // 客户端结构体大小// 传入 sizeof(buff) - 1 表示当前传输的是字符串预留一个位置存储 \0// 传入 0 表示当前是阻塞式读取ssizet n recvfrom(sock, buff, sizeof(buff) - 1, 0, (struct sockaddr *)peer, len);if (n 0)buff[n] \0;elsecontinue; // 继续读取// 2.处理数据std::string clientIp inet_ntoa(peer.sin_addr); // 获取IP地址uint16_t clientPort ntohs(peer.sin_port); // 获取端口号printf(Server get message from [%c:%d]\( %s\n, clientIp.c_str(), clientPort, buff);// 3.回响给客户端n sendto(sock_, buff, strlen(buff), 0, (const struct sockaddr *)peer, sizeof(peer));if (n -1)std::cout Send Message Fail: strerror(errno) std::endl;}}//.....}; } 万事具备后就可以启动服务器了可以看到服务器启动后处于阻塞等待状态这是因为还没有客户端给我的服务器发信息所以它就会暂时阻塞 如何证明服务器端正在运行 可以通过 Linux 中查看网络状态的指令因为我们这里使用的是 UDP 协议所以只需要输入下面这条指令就可以查看有哪些程序正在运行 netstat -nlup现在服务已经跑起来了并且如期占用了 8888 端口接下来就是编写客户端相关代码 注意0.0.0.0 表示任意IP地址 这个时候我们修改代码 sever.cc #include memory // 智能指针相关头文件 #include server.hppusing namespace std; using namespace nt_server;int main() {unique_ptrUdpServer usvr(new UdpServer(80));//使用智能指针创建了一个UdeServer对象// 初始化服务器usvr-InitServer();// 启动服务器usvr-StartServer();return 0; } 运行一下 我们去监控面板看看 不让我们绑定啊 事实上对于端口号的绑定[0,1023]这个区间的端口号都不要去绑定这些是系统内定的端口号一般都有固定的应用层协议使用比如http:80https:443…… 这个就像110就代表警察120是急救你的电话号码不能是这些吧。此外如果真的想要绑定那就使用sudo吧 3.3.5.命令行参数改装服务端 上面的代码中我们的端口号都是在代码里面指定了的但是我们不能每次使用的时候都去修改代码吧我们其实通过命令行参数来指定端口号 server.hpp //....// 退出码enum{SOCKET_ERR 1,BIND_ERR,USAGE_ERR}; //..... server.cc #include memory // 智能指针相关头文件 #include server.hppusing namespace std; using namespace nt_server;void Usage(const char* program) {cout Usage: endl;cout \t program ServerPort endl; }int main(int argc, char* argv[]) {if (argc ! 2){// 错误的启动方式提示错误信息Usage(argv[0]);return USAGE_ERR;}//命令行参数都是字符串我们需要将其转换成对应的类型uint16_t port stoi(argv[1]);//将字符串转换成端口号unique_ptrUdpServer usvr(new UdpServer(port));//使用智能指针创建了一个UdeServer对象// 初始化服务器usvr-InitServer();// 启动服务器usvr-StartServer();return 0; } 运行一下 很酷吧 3.4.客户端设计 3.4.1.使用命令行参数指定IP地址和端口号 和服务端不同客户端在运行时必须知道服务器的 IP 地址 和 端口号否则不知道自己该与谁进行通信所以对于 UdpClient 类来说ip 和 port 者两个字段是肯定少不了的 client.hpp 客户端头文件 #pragma once#include iostream #include stringnamespace nt_client {// 退出码enum{USAGE_ERR3};class UdpClient{public:// 构造UdpClient(const std::string ip, uint16_t port):server_ip_(ip), server_port_(port){} // 析构~UdpClient() {} // 初始化客户端void InitClient() {}// 启动客户端void StartClient() {}private:std::string server_ip_; // 服务器IP地址uint16_t server_port_; // 服务器端口号}; }这两个参数由用户主动传输这里就需要 命令行参数 相关知识了在启动客户端时需要以 ./client serverIp serverPort 的方式运行否则就报错并提示相关错误信息 client.cc 客户端源文件 #include iostream #include memory #include client.hppusing namespace std; using namespace nt_client;void Usage(const char* program) {cout Usage: endl;cout \t program ServerIP ServerPort endl; }int main(int argc, char* argv[]) {if (argc ! 3){// 错误的启动方式提示错误信息Usage(argv[0]);return USAGE_ERR;}//命令行参数都是字符串我们需要将其转换成对应的类型std::string ip argv[1];uint16_t port stoi(argv[2]);unique_ptrUdpClient usvr(new UdpClient(ip, port));// 初始化客户端usvr-InitClient();// 启动客户端usvr-StartClient();return 0; }如此一来只有正确的输入 [./client ServerIP ServerPort] 才能启动程序否则不让程序运行倒逼客户端启动时提供服务器的 IP 地址 和 端口号 3.4.2.初始化客户端 初始化客户端时同样需要创建 socket 套接字不同于服务器的是 客户端不需要自己手动绑定bind IP 地址与端口号 这是因为客户端手动指明bind 端口号 存在隐患如果恰好有两个程序使用了同一个端口会导致其中一方的客户端直接绑定失败无法运行将绑定 端口号 这个行为交给 OS 自动执行首次传输数据时自动 bind可以避免这种冲突的出现 为什么服务器要自己手动指定端口号并进行绑定bind 这是因为服务器的端口不能随意改变并且这是要公布给广大客户端看的同一家公司在部署服务时会对端口号的使用情况进行管理可以直接避免端口号冲突 客户端在启动前需要先知晓服务器的 sockaddr_in 结构体信息可以利用已知的 IP 地址 和 端口号 构建 这个就像顾客必须得知道哪里会提供服务吧 综上所述在初始化客户端时需要创建好套接字和初始化服务器的 sockaddr_in 结构体信息 也就是说 客户端需要bind吗需要只不过不需要用户显示的bind!一般有os自主随机选择一个端口号只能被1个进程bind对server是如此对client也是如此。其实clinent的port是多少不重要只要能保证主机上的唯一性就可以。系统什么时候给我bind呢首次发送数据的时候。 client.hpp 客户端头文件 #pragma once#include iostream #include string #include cstring #include cerrno #include cstdlib #include strings.h #include sys/types.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #include err.hppnamespace nt_client {// 退出码enum{SOCKET_ERR 1,BIND_ERR,USAGE_ERR};class UdpClient{public:// 构造UdpClient(const std::string ip, uint16_t port):server_ip_(ip), server_port_(port){} // 析构~UdpClient() {} // 初始化客户端void InitClient() {// 1.创建套接字sock_ socket(AF_INET, SOCK_DGRAM, 0);if(sock_ -1){std::cout Create Socket Fail: strerror(errno) std::endl;exit(SOCKET_ERR);}std::cout Create Success Socket: sock_ std::endl;// 2.构建服务器的 sockaddr_in 结构体信息bzero(svr_, sizeof(svr_));svr_.sin_family AF_INET; // 设置为网络通信PF_INET 也行svr_.sin_addr.s_addr inet_addr(server_ip_.c_str()); // 绑定服务器IP地址svr_.sin_port htons(server_port_); // 绑定服务器端口号//注意这里就不要自己去手动绑定了os会在我们第一次发送消息的时候自动绑定}// 启动客户端void StartClient() {}private:std::string server_ip_; // 服务器IP地址uint16_t server_port_; // 服务器端口号int sock_;struct sockaddr_in svr_; // 服务器的sockadder_in结构体信息}; }如此一来客户端就可以利用该 sockaddr_in 结构体与目标主机进行通信了 3.4.2.通信 接下来就是客户端向服务器发送消息消息由用户主动输入使用的是 sendto 函数 发送消息步骤 用户输入消息传入缓冲区、服务器相关参数使用 sendto 函数发送消息消息发送后客户端等待服务器回响消息 接收消息步骤 创建缓冲区接收信息判断是否接收成功处理信息 注同服务器一样客户端也需要不断运行 client.hpp // 启动客户端 void StartClient() {char buff[1024];while(true){// 1.发送消息std::string msg;std::cout Input Message# ;std::getline(std::cin, msg);ssize_t n sendto(sock_, msg.c_str(), msg.size(), 0, (const struct sockaddr*)svr_, sizeof(svr_));if(n -1){std::cout Send Message Fail: strerror(errno) std::endl;continue; // 重新输入消息并发送}// 2.接收消息socklen_t len sizeof(svr_); // 创建一个变量因为接下来的参数需要传左值n recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr*)svr_, len);if(n 0)buff[n] \0;elsecontinue;// 可以再次获取IP地址与端口号std::string ip inet_ntoa(svr_.sin_addr);uint16_t port ntohs(svr_.sin_port);printf(Client get message from [%s:%d]# %s\n,ip.c_str(), port, buff);} } 到现在已经大功告成了我们马上去运行 3.5.运行的一些问题 服务端必须先启动 我们去另外一个机器下面运行发现 我们发现已经建立成功了 我们现在来启动用户端 那么问题来了我们用户端怎么知道服务端的IP呢? 大家知道网络呢吧我们去上网搜索一下百度的官网 这个网址的本质就是IP地址也就是说服务端提供自己的服务的时候会把IP也一并公布出来 所以客户端是知道服务端的IP地址的我们直接填自己的公网IP地址即可 注127.0.0.1 表示本地环回通常用于测试网络程序因为我当前的服务器和客户端都是在同一机器上运行的所以就可以使用该 IP 地址当然直接使用服务器的公网 IP 地址也是可以的 我们运行一下 我们发现都没有反应啊 这其实不是代码的问题这是环境的问题我的云服务器我的环境的udp的端口没有开放 3.5.1.防火墙开放端口 打开防火墙 sudo systemctl start firewalld.service 关闭防火墙 sudo systemctl stop firewalld.service 查看防火墙状态 sudo firewall-cmd --state 开放TCP端口 sudo firewall-cmd --zonepublic --add-port80/tcp --permanent # 开放TCP 80端口 sudo firewall-cmd --zonepublic --add-port443/tcp --permanent # 开放TCP 443端口 sudo firewall-cmd --zonepublic --add-port3306/tcp --permanent # 开放TCP 3306端口 sudo firewall-cmd --zonepublic --add-port6379/tcp --permanent # 开放TCP 6379端口 关闭TCP端口 sudo firewall-cmd --zonepublic --remove-port80/tcp --permanent #关闭TCP 5672端口 sudo firewall-cmd --zonepublic --remove-port443/tcp --permanent #关闭TCP 443端口 sudo firewall-cmd --zonepublic --remove-port3306/tcp --permanent #关闭TCP 3306端口 sudo firewall-cmd --zonepublic --remove-port6379/tcp --permanent #关闭TCP 6379端口 开放udp端口 sudo firewall-cmd --zonepublic --add-port9595/udp --permanent # 开放UDP 9595端口 关闭UDP端口 sudo firewall-cmd --zonepublic --remove-port9595/udp--permanent #关闭UDP 9595端口 查看监听的TCP端口 netstat -ntlp 查看监听的UDP端口 netstat -nulp 配置生效 sudo firewall-cmd --reload 查看所有开放的端口 sudo firewall-cmd --list-port 检测UDP的特定端口是否开放 sudo firewall-cmd --query-port8877/udp 检测8877端口是否开放 检测TCP的特定端口是否开放 sudo firewall-cmd --query-port8877/tcp 检测8877端口是否开放 注意上面部分操作是要sudo权限的 1TCP和UDP的端口号范围都是0~65535。20~1023的端口号被预留给一些特定的服务和应用程序使用例如HTTP服务使用的端口号是80HTTPS服务使用的端口号是443FTP服务使用的端口号是21等等。这些端口号被称为“知名端口”或“系统端口”。31024~49151的端口号被称为“注册端口”或“用户端口”这些端口号可以被一些应用程序使用但是不会与系统端口冲突。449152~65535的端口号被称为“动态端口”或“私有端口”这些端口号可以被应用程序动态地分配使用。 好了我们现在来开放我们的8877窗口 我们去看看它有没有开放 没有啊这是因为我们还没有让我们的配置生效 好了目前我们已经把8877端口开放了接下来就是使用这个开放的端口了。 我们看看8877没有被占用 我们直接绑定8877 绑定成功了啊 我们输入信息看看 还是不通过 虽然通过CentOs 7系统的的「防火墙」开放了对应的端口号任然无法访问端口号对应的应用程序后面了解到原来还需要设置云服务器的「安全组规则」开放相应的端口权限服务端的接口才能真正开放。 3.5.2.设置云服务器安全组 华为云服务器开放端口的具体步骤 步骤1登录华为云官网 步骤2点击主页右上角的控制台 步骤3进去之后点击安全组 步骤4进去之后点击我们的实例 注意是点击我们的云服务器配置的那个实例 步骤5点击入方向规则 步骤 6点击添加规则 步骤7按照下面这样子填 注意 0.0.0.0/0表示任意IP地址。如果我们想开放8000-10000等其他区间也是可以的。 我们发现已经成功了 3.5.3.启动 到现在我们总算是完成所有步骤了 完美啊 这个时候网络通信已经完成了我们可以保持服务端一直开启然后多台云服务器启动client程序然后就能发给服务端了服务器这个时候就像是一个多人聊天室了 3.6.源代码 server.hpp #pragma once#include iostream #include string #include cstring #include cerrno #include cstdlib #include strings.h #include sys/types.h #include sys/socket.h #include netinet/in.h #include arpa/inet.hnamespace nt_server {// 退出码enum{SOCKET_ERR 1,BIND_ERR,USAGE_ERR};// 端口号默认值const uint16_t default_port 8888;class UdpServer{public:// 构造UdpServer(const uint16_t port default_port) : port_(port){}// 析构~UdpServer(){}// 初始化服务器void InitServer(){// 1.创建套接字sock_ socket(AF_INET, SOCK_DGRAM, 0);if (sock_ -1){std::cout Create Socket Fail: strerror(errno) std::endl;exit(SOCKET_ERR);}// 创建成功std::cout Create Success Socket: sock_ std::endl;// 2.绑定IP地址和端口号struct sockaddr_in local;bzero(local, sizeof(local)); // 置0// 填充字段local.sin_family AF_INET; // 设置为网络通信PF_INET 也行local.sin_port htons(port_); // 主机序列转为网络序列// local.sin_addr.s_addr inet_addr(ip_.c_str()); // 点分十进制转为短整数再将主机序列转为网络序列local.sin_addr.s_addr INADDR_ANY; // 绑定任何可用IP地址// 绑定IP地址和端口号int n bind(sock_, (const sockaddr *)local, sizeof(local));if (n 0){std::cout Bind IPPort Fail: strerror(errno) std::endl;exit(BIND_ERR);}// 绑定成功std::cout Bind IPPort Success std::endl;}// 启动服务器void StartServer(){// 服务器是不断运行的所以需要使用一个 while(true) 死循环char buff[1024]; // 缓冲区while (true){// 1. 接收消息struct sockaddr_in peer; // 客户端结构体socklen_t len sizeof(peer); // 客户端结构体大小// 传入 sizeof(buff) - 1 表示当前传输的是字符串预留一个位置存储 \0// 传入 0 表示当前是阻塞式读取ssize_t n recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr *)peer, len);if (n 0)buff[n] \0;elsecontinue; // 继续读取// 2.处理数据std::string clientIp inet_ntoa(peer.sin_addr); // 获取IP地址uint16_t clientPort ntohs(peer.sin_port); // 获取端口号printf(Server get message from [%s:%d]\) %s\n, clientIp.cstr(), clientPort, buff);// 3.回响给客户端n sendto(sock, buff, strlen(buff), 0, (const struct sockaddr )peer, sizeof(peer));if (n -1)std::cout Send Message Fail: strerror(errno) std::endl;}}private:int sock_; // 套接字uint16t port; // 端口号}; } server.cc #include memory // 智能指针相关头文件 #include server.hppusing namespace std; using namespace nt_server;void Usage(const char program) {cout Usage: endl;cout \t program ServerPort endl; }int main(int argc, char* argv[]) {if (argc ! 2){// 错误的启动方式提示错误信息Usage(argv[0]);return USAGE_ERR;}//命令行参数都是字符串我们需要将其转换成对应的类型uint16_t port stoi(argv[1]);//将字符串转换成端口号unique_ptrUdpServer usvr(new UdpServer(port));//使用智能指针创建了一个UdeServer对象// 初始化服务器usvr-InitServer();// 启动服务器usvr-StartServer();return 0; } client.hpp #pragma once#include iostream #include string #include cstring #include cerrno #include cstdlib #include strings.h #include sys/types.h #include sys/socket.h #include netinet/in.h #include arpa/inet.hnamespace nt_client {// 退出码enum{SOCKET_ERR 1,BIND_ERR,USAGE_ERR};class UdpClient{public:// 构造UdpClient(const std::string ip, uint16_t port):serverip(ip), serverport(port){} // 析构~UdpClient() {} // 初始化客户端void InitClient() {// 1.创建套接字sock_ socket(AF_INET, SOCKDGRAM, 0);if(sock -1){std::cout Create Socket Fail: strerror(errno) std::endl;exit(SOCKETERR);}std::cout Create Success Socket: sock std::endl;// 2.构建服务器的 sockaddrin 结构体信息bzero(svr, sizeof(svr));svr.sin_family AF_INET; // 设置为网络通信PFINET 也行svr.sin_addr.s_addr inet_addr(serverip.cstr()); // 绑定服务器IP地址svr.sin_port htons(serverport); // 绑定服务器端口号}// 启动客户端 void StartClient() {char buff[1024];while(true){// 1.发送消息std::string msg;std::cout Input Message# ;std::getline(std::cin, msg);ssizet n sendto(sock, msg.cstr(), msg.size(), 0, (const struct sockaddr*)svr, sizeof(svr_));if(n -1){std::cout Send Message Fail: strerror(errno) std::endl;continue; // 重新输入消息并发送}// 2.接收消息socklent len sizeof(svr); // 创建一个变量因为接下来的参数需要传左值n recvfrom(sock, buff, sizeof(buff) - 1, 0, (struct sockaddr*)svr, len);if(n 0)buff[n] \0;elsecontinue;// 可以再次获取IP地址与端口号std::string ip inetntoa(svr.sin_addr);uint16t port ntohs(svr.sin_port);printf(Client get message from [%s:%d]# %s\n,ip.c_str(), port, buff);} }private:std::string serverip; // 服务器IP地址uint16_t serverport; // 服务器端口号int sock_;struct sockaddrin svr; // 服务器的sockadder_in结构体信息}; } client.cc #include iostream #include memory #include client.hppusing namespace std; using namespace nt_client;void Usage(const char* program) {cout Usage: endl;cout \t program ServerIP ServerPort endl; }int main(int argc, char* argv[]) {if (argc ! 3){// 错误的启动方式提示错误信息Usage(argv[0]);return USAGE_ERR;}//命令行参数都是字符串我们需要将其转换成对应的类型std::string ip argv[1];uint16_t port stoi(argv[2]);unique_ptrUdpClient usvr(new UdpClient(ip, port));// 初始化客户端usvr-InitClient();// 启动客户端usvr-StartClient();return 0; } makefile .PHONY:all all:server clientserver:server.ccg -o \( \)^ -stdc11client:client.ccg -o \( \)^ -stdc11.PHONY:clean clean:rm -rf server client 怎么样收获很多吧 4.代码解耦 我们发现这个网络环境搭建和我们消息处理的代码即使我们上面没有提供消息处理业务的耦合度太高了我们能不能将这个网络环境搭建和我们这个聊天服务的代码进行解耦呢 基于模块化处理的思想将服务器中处理消息的函数与启动服务的函数解耦由程序员传入指定的回调函数服务器在启动时只需要传入对应的业务处理函数回调函数即可 这个时候就得使用C11的function包装器了 4.1.Cfunction包装器 我来带大家现学现用function定义在functional头文件中 function包装器是一种函数包装器也叫做适配器。它可以对可调用对象进行包装C中的function本质就是一个类模板。 我们看些例子 int f(int a, int b) {return a b; }struct Functor { public:int operator()(int a, int b){return a b;} };class Plus { public:static int plusi(int a, int b){return a b;}double plusd(double a, double b){return a b;} };int main() {// 1、包装函数指针函数名functionint(int, int) func1 f;cout func1(1, 2) endl;// 2、包装仿函数函数对象functionint(int, int) func2 Functor();cout func2(1, 2) endl;// 3、包装lambda表达式functionint(int, int) func3 {return a b; };cout func3(1, 2) endl;// 4、包装静态成员函数functionint(int, int) func4 Plus::plusi; // 可省略cout func4(1, 2) endl;// 5、包装类的非静态成员函数functiondouble(Plus, double, double) func5 Plus::plusd; // 不可省略cout func5(Plus(), 1.1, 2.2) endl;return 0; } 包装时指明返回值类型和各形参类型然后可调用对象赋值给function包装器即可包装后function对象就可以像普通函数一样使用了。取静态成员函数的地址可以不用取地址运算符 但取非静态成员函数地址使用 。包装费静态的成员函数需要注意非静态成员函数的第一个参数是隐藏this指针因此在包装时需要指明第一个形参的类型为类的类型。 好了相信大家会用了 如果想详细了解的话可以去http://t.csdnimg.cn/SRoDV 4.2.分离网络通信和消息处理业务 server.hpp 服务器头文件 #pragma once#include iostream #include string #include functional//注意这个 #include cstring #include cerrno #include cstdlib #include strings.h #include sys/types.h #include sys/socket.h #include netinet/in.h #include arpa/inet.hnamespace nt_server {// 退出码enum{SOCKET_ERR 1,BIND_ERR,USAGE_ERR};// 端口号默认值const uint16_t default_port 8888;using func_t std::functionstd::string(std::string); // 可以简单的理解为func_t是一个参数为string返回值同样为string的函数的类型class UdpServer{public:// 构造UdpServer(const func_t func, uint16_t port default_port)//注意这里的funct:port(port),serverHandle_(func) //注意serverHandle_的类型已经是一个funct,就是一个一个参数为string返回值同样为string的函数的类型{}// 析构~UdpServer(){} // 初始化服务器void InitServer(){// 1.创建套接字sock socket(AF_INET, SOCKDGRAM, 0);if(sock -1){std::cout Create Socket Fail: strerror(errno) std::endl;exit(SOCKETERR);}// 创建成功std::cout Create Success Socket: sock std::endl;// 2.绑定IP地址和端口号struct sockaddr_in local;bzero(local, sizeof(local)); // 置0// 填充字段local.sin_family AF_INET; // 设置为网络通信PF_INET 也行local.sinport htons(port); // 主机序列转为网络序列// local.sin_addr.s_addr inetaddr(ip.c_str()); // 点分十进制转为短整数再将主机序列转为网络序列local.sin_addr.s_addr INADDRANY; // 绑定任何可用IP地址// 绑定IP地址和端口号if(bind(sock, (const sockaddr)local, sizeof(local))){std::cout Bind IPPort Fail: strerror(errno) std::endl;exit(BIND_ERR);}// 绑定成功std::cout Bind IPPort Success std::endl;}// 启动服务器void StartServer(){// 服务器是不断运行的所以需要使用一个 while(true) 死循环char buff[1024]; // 缓冲区while(true){// 1. 接收消息struct sockaddr_in peer; // 客户端结构体socklen_t len sizeof(peer); // 客户端结构体大小// 传入 sizeof(buff) - 1 表示当前传输的是字符串预留一个位置存储 \0// 传入 0 表示当前是阻塞式读取ssizet n recvfrom(sock, buff, sizeof(buff) - 1, 0, (struct sockaddr)peer, len);if(n 0)buff[n] \0;elsecontinue; // 继续读取// 2.处理数据std::string clientIp inet_ntoa(peer.sin_addr); // 获取IP地址uint16_t clientPort ntohs(peer.sin_port); // 获取端口号printf(Server get message from [%s:%d]\( %s\n,clientIp.c_str(), clientPort, buff);// 获取业务处理后的结果std::string respond serverHandle_(buff);//特别注意这里业务处理的代码已经放到了这个serverHandle_这个函数了//而这个serverHandle_的函数是不在这个类里面是在类外面了//是在创建这个class UdpServer时就指定好了的// 3.回响给客户端n sendto(sock_, respond.c_str(), respond.size(), 0, (const struct sockaddr*)peer, sizeof(peer));if(n -1)std::cout Send Message Fail: strerror(errno) std::endl;}}private:int sock_; // 套接字uint16_t port_; // 端口号func_t serverHandle_; // 业务处理函数回调函数}; }我们得特别注意下面这几处地方 //... #include functional//注意这个//... namespace nt_server {//..using func_t std::functionstd::string(std::string); // 可以简单的理解为func_t是一个参数为string返回值同样为string的函数的类型class UdpServer{public:// 构造UdpServer(const func_t func, uint16_t port default_port)//注意这里的func_t:port_(port),serverHandle_(func) //注意serverHandle_的类型已经是一个func_t,就是一个一个参数为string返回值同样为string的函数的类型{}// 析构~UdpServer(){} // 初始化服务器void InitServer(){//...}// 启动服务器void StartServer(){//...// 获取业务处理后的结果std::string respond serverHandle_(buff);//特别注意这里业务处理的代码已经放到了这个serverHandle_这个函数了//而这个serverHandle_的函数是不在这个类里面是在类外面了//是在创建这个class UdpServer时就指定好了的}}private://...func_t serverHandle_; // 业务处理函数回调函数}; } 我们把消息处理业务分离了出来。 server.cc #include memory // 智能指针相关头文件 #include server.hppusing namespace std; using namespace nt_server;void Usage(const char* program) {cout Usage: endl;cout \t program ServerPort endl; }//消息处理业务 // 大写转小写英文字母 std::string UpToLow(const std::string resquest) {std::string ret(resquest);for(auto rc : ret){if(isupper(rc))rc 32;}return ret; }int main(int argc, char* argv[]) {if (argc ! 2){// 错误的启动方式提示错误信息Usage(argv[0]);return USAGE_ERR;}//命令行参数都是字符串我们需要将其转换成对应的类型uint16_t port stoi(argv[1]);//将字符串转换成端口号unique_ptrUdpServer usvr(new UdpServer(UpToLow,port));// 初始化服务器usvr-InitServer();// 启动服务器usvr-StartServer();return 0; }我们将消息处理业务设定为将消息从大写转小写。 我们运行一下看看 解耦很成功 好这个只是进行了字符串的转换处理 5.远程bash 服务端提供的服务可以千变万化例如我们可以提供命令服务实现一个远程的bash 5.1.popen和pclose bash 指令是如何执行的 接收指令字符串对指令进行分割构成有效信息创建子进程执行进程替换子进程运行结束后父进程回收僵尸进程输入特殊指令时的处理 这样子做太复杂了 其实Linux系统专门有这样一个系统调用接口——popen 函数 功能 popen()函数通过先创建一个管道然后调用 fork 产生一个子进程让子进程执行shell中的command命令。popen()建立的管道会连到子进程的标准输出设备(stdin)或标准输入设备(stdout)然后返回一个文件指针。随后进程便可利用此文件指针来读取子进程的输出设备或是写入到子进程的标准输入设备中。 这个函数做了这些事创建管道、创建子进程、执行指令、将执行结果以 FILE* 的形式返回 参数 1. command:要执行的命令。 2. type: 如果 type 为r则将子进程的标准输出stdout连接到返回的的文件指针。如果 type 为 w则将子进程的标准输入stdin连接到返回的的文件指针。 返回值 调用成功就返回一个文件指针如果此函数内部在调用 fork() 或 pipe() 失败或者不能分配内存将返回NULL。 此外有打开就有关闭——pclose函数 功能关闭popen()函数打开的文件 参数文件指针 返回值调用成功就返回0否则返回非0。 我们可以看一个例子 在下面的例子中我们使用 popen() 函数打开一个进程并执行 ls -l 命令然后将其输出作为文本流读取并打印到屏幕上。最后我们使用 pclose() 函数关闭进程和文件指针。 #include stdio.hint main() {FILE* fp popen(ls -l, r);//将执行结果以 FILE* 的形式返回if (!fp){perror(popen fail: );}char buf[1024];while (fgets(buf, sizeof(buf), fp) ! NULL)//将执行结果存到了buf里面{printf(%s, buf);}pclose(fp);return 0; }5.2.实现远程bash ExecCommand() 业务处理函数 — 位于 server.cc 服务器源文件 // 远程 bash std::string ExecCommand(const std::string request) {// 1.安全检查// ...// 2.获取执行结果FILE* fp popen(request.c_str(), r);//将执行结果以 FILE* 的形式返回if(fp NULL)return Cant execute command!;// 3.将结果读取至字符串中std::string ret;char buffline[1024]; // 行缓冲区while (fgets(buffline, sizeof(buffline), fp) ! NULL)//将执行结果存到buffline里面{// 将每一行结果添加至 ret 中ret buffline;}// 4.关闭文件流fclose(fp);// 5.返回最终执行结果return ret; }此时需要考虑一个问题如果别人输入的是敏感指令比如 rm -rf *怎么办 敏感操作包含这些kill 发送信号终止进程、mv 移动文件、rm 删除文件、while :; do 死循环、shutdown 关机等等 答案当然是直接拦截不让别人执行敏感操作毕竟 Linux 默认可没有回收站所以我们还需要考虑安全检查 在执行用户传入的指令前先对指令中的子串进行扫描如果发现敏感操作就直接返回不再执行后续操作 // 安全检查 bool checkSafe(const std::string comm) {// 构建安全检查组std::vectorstd::string unsafeComms{kill, mv, rm, while :; do, shutdown};// 查找 comm 中是否包含安全检查组中的字段for(auto str : unsafeComms){// 如果找到了就说明存在不安全的操作if(comm.find(str) ! std::string::npos)return false;}return true; }将 checkSafe 安全检查函数整合进 ExecCommand 业务处理函数中同时在构建 UdpServer 对象时传入该业务处理函数对象编译并运行程序 server.cc #include string #include vector #include memory // 智能指针相关头文件 #include cstdio #include server.hppusing namespace std; using namespace nt_server;// 安全检查 bool checkSafe(const std::string comm) {// 构建安全检查组std::vectorstd::string unsafeComms{kill, mv, rm, while :; do, shutdown};// 查找 comm 中是否包含安全检查组中的字段for(auto str : unsafeComms){// 如果找到了就说明存在不安全的操作if(comm.find(str) ! std::string::npos)return false;}return true; }// 远程 bash std::string ExecCommand(const std::string request) {// 1.安全检查if(!checkSafe(request))return Non-safety instructions, refusal to execute!;// 2.获取执行结果FILE* fp popen(request.c_str(), r);if(fp NULL)return Cant execute command!;// 3.将结果读取至字符串中std::string ret;char buffline[1024]; // 行缓冲区while (fgets(buffline, sizeof(buffline), fp) ! NULL){// 将每一行结果添加至 ret 中ret buffline;}// 4.关闭文件流fclose(fp);// 5.返回最终执行结果return ret; }void Usage(const char* program) {cout Usage: endl;cout \t program ServerPort endl; }int main(int argc, char* argv[]) {if (argc ! 2){// 错误的启动方式提示错误信息Usage(argv[0]);return USAGE_ERR;}//命令行参数都是字符串我们需要将其转换成对应的类型uint16_t port stoi(argv[1]);//将字符串转换成端口号unique_ptrUdpServer usvr(new UdpServer(ExecCommand,port));// 初始化服务器usvr-InitServer();// 启动服务器usvr-StartServer();return 0; } 可以看到输入安全指令时可以正常获取结果如果输入的是非安全指令会直接拒绝执行 诸如 cd 这种指令称为 内建命令是需要特殊处理的所以这里才会执行失败 这样子就完成一个简易的远程bash啦 这里其实就是想告诉大家我们在Xshell和云服务器的工作原理是我们的Xshell就相当于我们的client端我们的云服务器相当于我们的server端我们的云服务器开放一个端口让我们去访问我们在Xshell输入指令但是指令的运行是在很远的另外一台服务器服务器通过网络来将结果打印到我们的Xshell里面而已 我们可以去看看这个开放的端口 就是上面那个323 5.3.windows和Linux联动 我们说过每款操作系统是不一样的但是每款操作系统的网络协议栈必须是一样的所以Linux的套接字和Windows的套接字是一样的也就是说我们可以在windows环境下面基本可以运行这代码只不过windows里的数据类型和Linux有点区别 接下来我要简单的修改一下代码让我们的Linux端充当一个服务器让windows下充当一个客户端 我们打开vs2022 由于Linux端的代码和windows端的代码有点不一样我们需要稍微做一点修改 client.hpp #pragma once#pragma warning (disable:4996)#include iostream #include string #include cstring #include cerrno #include cstdlib #include string.h #include sys/types.h #includewinsock2.h #pragma comment(lib,ws2_32.lib)namespace nt_client {// 退出码enum{SOCKET_ERR 1,BIND_ERR,USAGE_ERR};class UdpClient{public:// 构造UdpClient(const std::string ip, uint16_t port):server_ip_(ip), server_port_(port){}// 析构~UdpClient(){}// 初始化客户端void InitClient(){// 1.创建套接字sock_ socket(AF_INET, SOCK_DGRAM, 0);if (sock_ -1){std::cout Create Socket Fail: strerror(errno) std::endl;exit(SOCKET_ERR);}std::cout Create Success Socket: sock_ std::endl;// 2.构建服务器的 sockaddr_in 结构体信息memset(svr_,0, sizeof(svr_));svr_.sin_family AF_INET; // 设置为网络通信PF_INET 也行svr_.sin_addr.s_addr inet_addr(server_ip_.c_str()); // 绑定服务器IP地址svr_.sin_port htons(server_port_); // 绑定服务器端口号}// 启动客户端void StartClient(){char buff[1024];while (true){// 1.发送消息std::string msg;std::cout Input Message# ;std::getline(std::cin, msg);int n sendto(sock_, msg.c_str(), msg.size(), 0, (const struct sockaddr*)svr_, sizeof(svr_));if (n -1){std::cout Send Message Fail: strerror(errno) std::endl;continue; // 重新输入消息并发送}// 2.接收消息int len sizeof(svr_); // 创建一个变量因为接下来的参数需要传左值n recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr*)svr_, len);if (n 0)buff[n] \0;elsecontinue;// 可以再次获取IP地址与端口号std::string ip inet_ntoa(svr_.sin_addr);uint16_t port ntohs(svr_.sin_port);printf(Client get message from [%s:%d]# %s\n, ip.c_str(), port, buff);}}private:std::string server_ip_; // 服务器IP地址uint16_t server_port_; // 服务器端口号SOCKET sock_;struct sockaddr_in svr_; // 服务器的sockadder_in结构体信息}; } client.cpp #define _CRT_SECURE_NO_WARNINGS/*客户端*/ #includememory #includeudp_client.husing namespace std; using namespace nt_client;#define SERVER_IP 127.0.0.1 // 服务器IP地址:指要连接的服务器的地址 #define SERVER_PORT 8877 // 服务器端口号int main() {// 初始化Winsock库WSADATA wsaData;if (WSAStartup(MAKEWORD(2, 2), wsaData) ! 0) {printf(WSAStartup failed!\n);return -1;}unique_ptrUdpClient usvr(new UdpClient(SERVER_IP, SERVER_PORT));// 初始化客户端usvr-InitClient();// 启动客户端usvr-StartClient();// 清理Winsock库WSACleanup();return 0; } 我们打开浏览器 完美通信了 这里我只是想说明我们的windows和Linux的网络协议栈是一样的 6.多人聊天室 6.1.单线程版本 这是基于 UDP 协议实现的最后一个网络程序主要功能是 构建一个多人聊天室当某个用户发送消息时其他用户可以立即收到形成一个群聊 在这个程序中服务器扮演了一个接收消息和分发消息的中间角色将消息发送给已知的用户主机 首先我们需要明确一些问题我们服务端怎么知道客户端的位置 其实服务端在接受客户端的信息的时候就已经有它的信息了 但是我们的用户不只有1个我们必须得把它们的信息管理起来我们必须弄一张用户列表来记录我们的在线用户信息这里我们使用unordered_map不知道的可以去http://t.csdnimg.cn/SJH7p server.hpp #pragma once#include iostream #include string #include functional//注意这个 #include cstring #include cerrno #include cstdlib #include strings.h #include sys/types.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #includeunordered_mapnamespace nt_server {// 退出码enum{SOCKET_ERR 1,BIND_ERR,USAGE_ERR};// 端口号默认值const uint16_t default_port 8888;using func_t std::functionstd::string(std::string); // 可以简单的理解为func_t是一个参数为string返回值同样为string的函数的类型class UdpServer{public:// 构造UdpServer(const func_t func, uint16_t port default_port)//注意这里的func_t:port_(port),serverHandle_(func) //注意serverHandle_的类型已经是一个func_t,就是一个一个参数为string返回值同样为string的函数的类型{}// 析构~UdpServer(){} // 初始化服务器void InitServer(){// 1.创建套接字sock_ socket(AF_INET, SOCK_DGRAM, 0);if(sock_ -1){std::cout Create Socket Fail: strerror(errno) std::endl;exit(SOCKET_ERR);}// 创建成功std::cout Create Success Socket: sock_ std::endl;// 2.绑定IP地址和端口号struct sockaddr_in local;bzero(local, sizeof(local)); // 置0// 填充字段local.sin_family AF_INET; // 设置为网络通信PF_INET 也行local.sin_port htons(port_); // 主机序列转为网络序列// local.sin_addr.s_addr inet_addr(ip_.c_str()); // 点分十进制转为短整数再将主机序列转为网络序列local.sin_addr.s_addr INADDR_ANY; // 绑定任何可用IP地址// 绑定IP地址和端口号if(bind(sock_, (const sockaddr*)local, sizeof(local))){std::cout Bind IPPort Fail: strerror(errno) std::endl;exit(BIND_ERR);}// 绑定成功std::cout Bind IPPort Success std::endl;}//检测是不是新用户void CheckUser(const struct sockaddr_in client,const std::string clientIp_,uint16_t clientPort_){auto iter online_user_.find(clientIp_);if(iteronline_user_.end()){online_user_.insert({clientIp_,client});std::cout[clientIp_:clientPort_] add to oniline user.std::endl;}}//广播给所有人void Broadcast(const std::string respond,const std::string clientIp_,uint16_t clientPort_){for(const autousr :online_user_){std::string message[;messageclientIp_;message : ;messagestd::to_string(clientPort_);message]#;messagerespond;int z sendto(sock_, respond.c_str(), respond.size(), 0, (const sockaddr*)usr.second, sizeof(usr.second));if(z -1)std::cout Send Message Fail: strerror(errno) std::endl;}}// 启动服务器void StartServer(){// 服务器是不断运行的所以需要使用一个 while(true) 死循环char buff[1024]; // 缓冲区while(true){// 1. 接收消息struct sockaddr_in client; // 客户端结构体socklen_t len sizeof(client); // 客户端结构体大小// 传入 sizeof(buff) - 1 表示当前传输的是字符串预留一个位置存储 \0// 传入 0 表示当前是阻塞式读取ssize_t n recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr*)client, len);if(n 0)buff[n] \0;elsecontinue; // 继续读取// 2.处理数据std::string clientIp inet_ntoa(client.sin_addr); // 获取用户的IP地址uint16_t clientPort ntohs(client.sin_port); // 获取端口号//2.1.判断是不是新用户如果是就加入如果不是就什么也不做CheckUser(client,clientIp,clientPort); //2.2 对数据进行业务处理并获取业务处理后的结果std::string respond serverHandle_(buff);//特别注意这里业务处理的代码已经放到了这个serverHandle_这个函数了// 3.回响给所有在线客户端Broadcast(respond,clientIp,clientPort); }}private:int sock_; // 套接字uint16_t port_; // 端口号func_t serverHandle_; // 业务处理函数回调函数std::unordered_mapstd::string,struct sockaddr_in online_user_;//在线用户列表}; } 其他都没有变化 server.cc #include string #include vector #include memory // 智能指针相关头文件 #include cstdio #include server.hppusing namespace std; using namespace nt_server;//业务处理函数 std::string ExecCommand(const std::string request) {return request; }void Usage(const char* program) {cout Usage: endl;cout \t program ServerPort endl; }int main(int argc, char* argv[]) {if (argc ! 2){// 错误的启动方式提示错误信息Usage(argv[0]);return USAGE_ERR;}//命令行参数都是字符串我们需要将其转换成对应的类型uint16_t port stoi(argv[1]);//将字符串转换成端口号unique_ptrUdpServer usvr(new UdpServer(ExecCommand,port));// 初始化服务器usvr-InitServer();// 启动服务器usvr-StartServer();return 0; } 我们运行一下 到这里我们还是感觉挺正常的我们接着往下看 我们再运行一下我们在windows上面写的client程序 有没有发现我们第二个客户发消息过去第二个客户只收到了它自己的消息第一个客户也没有反应 但是我们继续让第一个客户发消息我们却惊奇的发现这次收到的消息居然是第二个用户第一次发的消息 这是为什么呢 这是因为我们仔细观察一下我们的client端代码 // 启动客户端 void StartClient() {char buff[1024];while(true){// 1.发送消息std::string msg;std::cout Input Message# ;std::getline(std::cin, msg);......n recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr*)svr_, len);..... } } 每获取完一次消息就立马要去输入如果我们不输入那么这个getline函数输入会阻塞这个进程的运行啊。 那么就有2种解决方法客户端的代码 用其他不阻塞的函数替换getline比如getch()和kbhit()函数将客户端的代码从单线程版改成多线程版本一个线程输入一个线程获取信息 注意 我们创建的是UDP套接字它是全双工的也就是说它是可以同时读和写的。 那么话不多说我们就用多线程来修改一下 6.2.多线程版本 6.1.1.C11类内使用多线程 有很多时候我们希望可以在C类里面对那些比较耗时的函数使用多线程技术但是熟悉C对象语法的人应该知道C类的成员函数的函数指针不能直接做为参数传到pthread_create,主要因为是C成员函数指针带有类命名空间同时成员函数末尾是会被C编译器加上可以接收对象地址的this指针参数。 因此需要将成员函数做一定的转化将其转化为不被编译器加上this指针而由我们自己来为该函数维护this指针即可。 只需将类内的线程函数变化为static函数但是static函数就不能直接访问到我们类内的私有数据不要忘了这个线程函数的void*参数我们可以把this指针作为参数传给它然后就能通过这个this指针来访问到我们的类内的私有数据 client.hpp #pragma once#include iostream #include string #include cstring #include cerrno #include cstdlib #include strings.h #include sys/types.h #include sys/socket.h #include netinet/in.h #include pthread.h #include unistd.h #include arpa/inet.h #includefunctionalnamespace nt_client {// 退出码enum{SOCKET_ERR 1,BIND_ERR,USAGE_ERR};class UdpClient{public:// 构造UdpClient(const std::string ip, uint16_t port): server_ip_(ip), server_port_(port){}// 析构~UdpClient(){}static void *send_message(void *argc)//传进来的是this指针{UdpClient*_this (UdpClient*)argc;//强制转换为类指针char buff[1024];while (1){// 1.发送消息std::string msg;std::cout Input Message# ;std::getline(std::cin, msg);ssize_t n sendto(_this-sock_, msg.c_str(), msg.size(), 0, (const struct sockaddr *)_this-svr_, sizeof(_this-svr_));if (n -1){std::cout Send Message Fail: strerror(errno) std::endl;continue; // 重新输入消息并发送}}return (void*)0;}static void *recv_message(void *argc)//传进来的是this指针{UdpClient*_this (UdpClient*)argc;char buff[1024];while (1){// 2.接收消息socklen_t len sizeof(_this-svr_); // 创建一个变量因为接下来的参数需要传左值int n recvfrom(_this-sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr *)_this-svr_, len);if (n 0)buff[n] \0;elsecontinue;// 可以再次获取IP地址与端口号std::string ip inet_ntoa(_this-svr_.sin_addr);uint16_t port ntohs(_this-svr_.sin_port);printf(Client get message from [%s:%d]# %s\n, ip.c_str(), port, buff);}return (void*)0;}// 初始化客户端void InitClient(){// 1.创建套接字sock_ socket(AF_INET, SOCK_DGRAM, 0);if (sock_ -1){std::cout Create Socket Fail: strerror(errno) std::endl;exit(SOCKET_ERR);}std::cout Create Success Socket: sock_ std::endl;// 2.构建服务器的 sockaddr_in 结构体信息bzero(svr_, sizeof(svr_));svr_.sin_family AF_INET; // 设置为网络通信PF_INET 也行svr_.sin_addr.s_addr inet_addr(server_ip_.c_str()); // 绑定服务器IP地址svr_.sin_port htons(server_port_); // 绑定服务器端口号}// 启动客户端void StartClient(){pthread_t recv, sender;//只需将类内的线程函数变化为static函数但是static函数就不能直接访问到我们类内的私有数据//不要忘了这个线程函数的void*参数我们可以把this指针作为参数传给它//然后就能通过这个this指针来访问到我们的类内的私有数据pthread_create(recv, nullptr, recv_message, (void*)this);pthread_create(sender, nullptr, send_message, (void*)this);pthread_join(recv,nullptr);pthread_join(sender,nullptr);}private:std::string server_ip_; // 服务器IP地址uint16_t server_port_; // 服务器端口号int sock_;//套接字描述符struct sockaddr_in svr_; // 服务器的sockadder_in结构体信息}; } client.cc保持不变 makefile .PHONY:all all:server clientserver:server.ccg -o \) \(^ -stdc11 -lpthreadclient:client.ccg -o \) $^ -stdc11 -lpthread.PHONY:clean clean:rm -rf server client 6.1.2.C11多线程功能 这里需要简单的使用一下C11的多线程功能 大家跟我简单学一下就行 #include iostream #include thread //必须包含thread头文件void threadFunctionA() {std::cout Run New thread: 1 std::endl; } void threadFunctionB(int n) {std::cout Run New thread: n std::endl; }int main() {std::cout Run Main Thread std::endl;std::thread newThread1(threadFunctionA);std::thread newThread2(threadFunctionB,2);newThread1.join();newThread2.join();return 0; }//result Run Main Thread Run New thread: 1 Run New thread: 2上述示例中我们创建了两个线程newThread1和newThread2使用函数threadFunctionA()和threadFunctionB()作为线程的执行函数并使用join()函数等待线程执行完成。 windows端client.h代码 #pragma once#pragma warning (disable:4996)#include iostream #include string #include cstring #include cerrno #include cstdlib #include string.h #include sys/types.h #includewinsock2.h #include thread #pragma comment(lib,ws2_32.lib)namespace nt_client {// 退出码enum{SOCKET_ERR 1,BIND_ERR,USAGE_ERR};class UdpClient{public:// 构造UdpClient(const std::string ip, uint16_t port): serverip(ip), serverport(port){}// 析构~UdpClient(){}static void* send_message(void* argc)//传进来的是this指针{UdpClient* _this (UdpClient)argc;//强制转换为类指针char buff[1024];while (1){// 1.发送消息std::string msg;std::cout Input Message# ;std::getline(std::cin, msg);int n sendto(this-sock, msg.c_str(), msg.size(), 0, (const struct sockaddr)this-svr, sizeof(this-svr));if (n -1){std::cout Send Message Fail: strerror(errno) std::endl;continue; // 重新输入消息并发送}}return (void)0;}static void recv_message(void* argc)//传进来的是this指针{UdpClient* _this (UdpClient)argc;char buff[1024];while (1){// 2.接收消息int len sizeof(this-svr); // 创建一个变量因为接下来的参数需要传左值int n recvfrom(this-sock, buff, sizeof(buff) - 1, 0, (struct sockaddr)this-svr, len);if (n 0)buff[n] \0;elsecontinue;// 可以再次获取IP地址与端口号std::string ip inet_ntoa(this-svr.sin_addr);uint16_t port ntohs(this-svr.sin_port);printf(Client get message from [%s:%d]# %s\n, ip.cstr(), port, buff);}return (void*)0;}// 初始化客户端void InitClient(){// 1.创建套接字sock socket(AF_INET, SOCKDGRAM, 0);if (sock -1){std::cout Create Socket Fail: strerror(errno) std::endl;exit(SOCKETERR);}std::cout Create Success Socket: sock std::endl;// 2.构建服务器的 sockaddrin 结构体信息memset(svr,0, sizeof(svr));svr.sin_family AF_INET; // 设置为网络通信PFINET 也行svr.sin_addr.s_addr inet_addr(serverip.cstr()); // 绑定服务器IP地址svr.sin_port htons(serverport); // 绑定服务器端口号}// 启动客户端void StartClient(){//只需将类内的线程函数变化为static函数但是static函数就不能直接访问到我们类内的私有数据//不要忘了这个线程函数的void参数我们可以把this指针作为参数传给它//然后就能通过这个this指针来访问到我们的类内的私有数据std::thread rec(recv_message, (void)this);std::thread sen(send_message, (void*)this);rec.join();sen.join();}private:std::string serverip; // 服务器IP地址uint16_t serverport; // 服务器端口号int sock_;//套接字描述符struct sockaddrin svr; // 服务器的sockadder_in结构体信息}; } 我们运行起来看看 这样子客户端就能进行一个群聊了。 这回就完全没有什么问题了但是它这个输入和输出混在一起了客户体验感不好。 6.3.分离输入输出 我们知道Linux下一切皆是文件那么我们的终端也是文件我们一个账号可以被多个终端同时登录就像下面这样子 那Linux是怎么区分的呢 其实在/dev/pts这个目录里面就记录了登录这个账号的终端 我们可以做下面这些测试 我们发现往0立马写没有反应往1写是左边那个终端往2写是右边这个终端 我们可以让它当作我们的消息的输出区我们可以使用open来打开这个文件然后往里面写入 我们写一个测试代码 #includeiostream #includeunistd.h #includesys/types.h #includesys/stat.h #includefcntl.hstd::string termial/dev/pts/1;int main() {int fdopen(termial.c_str(),O_WRONLY);if(fd0){std::coutopen errorstd::endl;return 1;}dup2(fd,1);//重定向std::couthello worldstd::endl;close(fd); } 我们发现在右边的终端运行这个程序真的打印到左边这个终端来了。 client.hpp ….. static void *recv_message(void argc)//传进来的是this指针{…….std::cerrClient get message from [ip.c_str():port]#buffstd::endl;//注意这个cerr}return (void)0;} 我们运行看看 很好啊输入和输出就分离了 6.5.上线通知在线用户 我们聊天要看看对方在不在线 client.hpp static void *send_message(void argc)//传进来的是this指针{UdpClient_this (UdpClient*)argc;//强制转换为类指针std::string msg1自己的IP;msg1comming….;sendto(this-sock, msg1.c_str(), msg1.size(), 0, (const struct sockaddr )this-svr, sizeof(this-svr));char buff[1024];while (1){//。。。}return (void)0;} 这样子你一上线就会通知大家 至此基于 UDP 协议实现的多个网络程序都已经编写完成了尤其是 多人聊天室如果加上简单的图形化界面比如 EasyX、EGE就是一个简易版的 QQ 群聊
- 上一篇: 有什么做兼职的网站比较好网站营销推广策划方案
- 下一篇: 有数据库的网站php做网站代码
相关文章
-
有什么做兼职的网站比较好网站营销推广策划方案
有什么做兼职的网站比较好网站营销推广策划方案
- 技术栈
- 2026年04月20日
-
有什么做服装的网站做家政公司网站
有什么做服装的网站做家政公司网站
- 技术栈
- 2026年04月20日
-
有什么网站做投标设计wordpress内容付费模板
有什么网站做投标设计wordpress内容付费模板
- 技术栈
- 2026年04月20日
-
有数据库的网站php做网站代码
有数据库的网站php做网站代码
- 技术栈
- 2026年04月20日
-
有网络网站打不开怎么回事打开网站后直接做跳转页面
有网络网站打不开怎么回事打开网站后直接做跳转页面
- 技术栈
- 2026年04月20日
-
有网站代码怎么建站wordpress加载很慢
有网站代码怎么建站wordpress加载很慢
- 技术栈
- 2026年04月20日
