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

十八把网站做扇子免费下载微信

十八把网站做扇子,免费下载微信,河北wap网站建设,网页设计公司哪里会需要翻译本篇博客整理了 socket 套接字编程的相关内容#xff0c;包括 socket 网络通信原理、socket 相关的系统调用接口等#xff0c;分别演示了基于UDP协议、TCP协议的 socket 网络编程#xff0c;旨在让读者更加深入理解网络通信原理和设计#xff0c;对网络编程有初步的认识和掌…        本篇博客整理了 socket 套接字编程的相关内容包括 socket 网络通信原理、socket 相关的系统调用接口等分别演示了基于UDP协议、TCP协议的 socket 网络编程旨在让读者更加深入理解网络通信原理和设计对网络编程有初步的认识和掌握。 目录 一、Socket 通信 1源与目的系列 .1- 源 IP 地址 vs 目的 IP 地址 .2- 源 MAC 地址 vs 目的 MAC 地址 .3- 源端口号 vs 目的端口号 2socket 与网络通信本质 3TCP 协议和 UDP 协议 4网络字节序为大端 二、Socket 系列接口 1TCP/UDP 通用接口 .1- 创建套接字 socket() .2- 绑定端口号 bind() 2UDP 专用接口 3TCP 专用接口 4sockaddr 结构体         5IP 地址的表现形式 三、基于 UDP 协议的网络编程 1服务端 .1- 创建套接字 .2- 绑定套接字 .3- 运行 2客户端 .1- 创建套接字 .2- 运行 3本地测试 4网络测试 .1- 绑定INADDR_ANY .2- 测试效果 5完整代码 四、基于 TCP 协议的网络编程 1服务端 .1- 创建套接字 .2- 绑定 .3- 监听 .4- 获取数据请求 .5- 处理数据请求 2客户端 .1- 创建套接字 .2- 连接服务端 .3- 发送数据请求 3单执行流服务端并不实用 4多进程版本的服务端 .1- 捕捉信号版 .2- 爷孙进程版 5多线程版本的服务端 6线程池版本的服务端 .1- 多线程版本服务端的弊病 .2- 线程池的引入 .3- 测试效果 一、Socket 通信 1源与目的系列 在广域网中进行通信例如一个局域网内的一台主机要给另一台主机发送数据离不开局域网之间的路由器。 总得来说在广域网的通信过程大致为主机到路由器再到主机但在实际中数据传输过程中主机到主机之间的路由器可能有若干个使得数据传输过程更加复杂而其复杂主要在于如何确定数据传输的去向。 数据在传输过程中一台主机到另一台主机这是确定的也就是说数据的最终去向是确定不变的但数据并不会直接从一台主机传输到另一台主机而是要在途中经由路由器那么一台主机到一台路由器、一台路由器到另一台路由器、最终到另一台主机这个过程中数据的去向是始终在变化的。 为了确定数据在传输过程的去向以更好地完成数据传输前人引入了 IP 地址、MAC 地址、端口号等概念。 .1- 源 IP 地址 vs 目的 IP 地址 因特网上的每台计算机都有一个唯一的 IP 地址主要用来表明数据的最终去向或最初来源。 如果有一台主机要向另一台主机传输数据那么接收数据的主机的 IP 地址就会在数据传输过程中被当作目的 IP 地址但只知道目的 IP 地址是不够的在接收数据的主机收到数据后还会对发送数据的主机做出响应具体方式是给其发送数据因此也要知道发送数据的主机的IP地址也就是源 IP 地址。 一份正在传输的数据中会包含其源 IP 地址和目的 IP 地址其中目的 IP 地址用来表明数据的最终去向而源 IP 地址作为对端主机响应时的目的 IP 地址。 数据在开始传输之前会先被自顶向下地贯穿网络协议栈完成封装在网络层封装的 IP 报头中就包含了源 IP 地址和目的 IP 地址。 总得来说数据的源 IP 地址和目的 IP 地址在数据传输之初就已确定在传输过程中一般不会改变。 .2- 源 MAC 地址 vs 目的 MAC 地址 在广域网的通信中数据在传输过程中要经过若干个路由器才能到达对端主机。由于广域网是由若干个局域网组成的那么在广域网的通信中数据的传输势必涉及跨局域网。为了支持数据更好地在局域网之间传输前人引入了 MAC 地址主要用来表明数据的最近去向或最近来源。 在局域网中每台计算机都绑定了一张网卡因此每台计算机都有一个只属于自己的 MAC 地址以表明自己在某个局域网中的唯一性。 跨局域网的通信与路由器息息相关。一个路由器至少能够横跨两个局域网而被路由器级联的局域网都认为这个级联它们的路由器是自己局域网内部的一台主机因此路由器可以和这些局域网内的任意一台主机直接进行通信。 在跨局域网的通信中一台主机向另一台主机传输的数据并不是直接发送给另一台主机的而要先经由路由器再送到另一台主机。 在数据被主机发送给路由器的过程中先会自顶向下地贯穿主机网络协议栈完成封装尤其在主机的数据链路层中封装上包含源 MAC 地址和目的 MAC 地址的报头再被发送给路由器其中源 MAC 地址是主机自己的 MAC 地址而目的 MAC 地址则是接收数据的路由器的 MAC 地址。 收到主机发来的数据后路由器会先使数据自底向上地贯穿网络协议栈完成解包从数据链路层到网络层再使数据自顶向下地贯穿网络协议栈完成封装。同样的在路由器的数据链路层中封装上包含源 MAC 地址和目的 MAC 地址的报头但此时的源 MAC 地址和目的 MAC 地址相较于路由器解包数据前已经发生了变化此时的源 MAC 地址其实是当前路由器的 MAC 地址而此时的目的 MAC 地址其实变成了下一台要接收数据的路由器或主机的 MAC 地址。完成封装后当前路由器会将数据发送给下一台要接收数据的路由器或主机。 总得来说数据的源 MAC 地址和目的 MAC 地址会随着数据的传输因路由器不断的解包和封装而不断发生变化。 .3- 源端口号 vs 目的端口号 通过 IP 地址和 MAC 地址已经能够很好地完成数据传输但在实际上的数据传输过程中数据的传输并非在一台主机与另一台主机之间更准确地说数据的发送者是一台主机上的一个进程或称客户进程client而数据的接收者是另一台对端主机上的一个进程或称服务进程server。 在两台正在通信的主机上可能同时存在多个正在进行跨网络通信的进程因此在数据到达服务端主机之后必须要通过某种方法找到该主机上对应的服务进程并将数据交给服务进程处理在服务进程处理完数据之后还要对客户端进行响应因此服务端主机也需要知道具体是客户端上的哪一个客户进程向它发送了数据请求。 而端口号就是用来标识一台主机上的一个进程的。 【Tips】端口号port的特征 端口号是传输层协议的内容。端口号是一个 2 字节 16 位的整数。端口号用于标识一个进程以告知操作系统当前数据要交由主机中的哪一个进程来处理。一个进程可以同时绑定多个端口号但一个端口号不能同时被多个进程绑定。由于端口号是隶属于某台主机的因此可以在两台不同的主机之间重复但绝不能在同一台主机上进行网络通信的进程之间重复。 由于 IP 地址能够唯一地标识公网内的一台主机端口号则能够唯一地标识一台主机上的一个进程因此“IP 地址 端口号”能够唯一地标识网络中的某一台主机的某一个进程。 当数据在传输层进行封装时会被封装上对应的源端口号和目的端口号此时通过源 IP 地址和源端口号就能够在网络中唯一地标识发送数据的进程而通过目的 IP 地址和目的端口号就能够在网络上唯一地标识接收数据的进程以此实现跨网络的进程间通信。 【补】进程PID vs 端口号         进程 PID 用于标识系统内所有进程的唯一性是属于系统级的概念端口号则用于标识对外进行网络数据请求的进程的唯一性是属于网络的概念。         它们本身都用于标识一个进程的唯一性。这就好比一个人在不同场合下有不同的身份信息但这些信息都能指向这个人只是发挥作用的场合有区别罢了。例如在国家的行政管理下这个人拥有仅属于自己的身份证号在学校的管理下这个人拥有仅属于自己的学号或在公司的管理下这个人拥有仅属于自己的工号。         在底层实际上进程 PID 和端口号是采用哈希的方式建立了一种映射关系。当底层拿到一个端口号时就可以通过对应的哈希算法找到其所映射的进程 PID进而找到其所对应的进程。 2socket 与网络通信本质 socket 直译为中文有“插座”的意思。在现实生活中只有插座上的一个插孔与一个插头的规格相适配才能完成某种电流传输的需求正如在进行网络通信时一台客户端上的一个客户进程能够通过“IP 地址 端口号”适配地找到一台服务端上的一个服务进程以完成数据的传输和处理。 而“IP 地址 端口号”其实就是套接字 socket 。 网络通信也就是 socket 通信本质上就是进程间的通信。 【Tips】网络通信的本质进程间通信。 要实现进程间通信就必须存在共享资源而网络恰恰就是一种共享资源。进行网络通信其实是在做 IO因此所有上网的行为基本可以分为两种①把数据发出去、 ②把数据读回来。 3TCP 协议和 UDP 协议 网络协议栈是贯穿整个计算机体系结构的在应用层、操作系统层和驱动层都有一部分而协议栈的每一层都有对应的网络协议。在进行网络编程、使用系统调用接口实现网络数据通信时一定会涉及传输层的协议其中最典型的两种即 TCP 协议和 UDP 协议。 【Tips】TCP协议Transmission Control Protocol传输控制协议 TCP协议是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP协议是面向连接的如果两台主机之间想要进行数据传输那么必须要先建立连接只有在连接建立成功后才能进行数据传输。TCP协议是保证可靠的协议如果数据在传输过程中出现了丢包、乱序等情况TCP协议都有对应的解决方法。 【Tips】UDP协议User Datagram Protocol用户数据报协议 UDP协议是一种无需建立连接的、不可靠的、面向数据报的传输层通信协议。使用UDP协议进行通信时无需建立连接如果两台主机之间想要进行数据传输那么直接将数据发送给对端主机就行了。UDP协议是不可靠的如果数据在传输过程中出现了丢包、乱序等情况UDP协议本身是无法得知的。 既然使用可靠的 TCP 协议能够在一定程度上保证数据传输时的可靠性那么不可靠的 UDP 协议的存在又有什么意义呢 虽然 TCP 协议是一种可靠的传输协议但为了维护所谓的可靠它就需要在底层做更多的工作因此 TCP 协议底层的实现是比较复杂的数据的传输更稳定但过程更缓慢。 而 UDP 协议虽然是一种不可靠的传输协议但它在底层并不需要做过多的工作因此 UDP协议在底层的实现一定更加简单数据的传输不稳定但过程更迅速。 进行网络编程时要采用 TCP 协议还是 UDP 协议具体取决于上层的应用场景。 如果应用场景严格要求数据在传输过程中的可靠性就必须采用TCP协议如果应用场景允许数据在传输过程中出现少量丢包那么肯定优先选择 UDP 协议。 而一些优秀的网站在设计网络通信算法时会同时采用 TCP 协议和 UDP 协议并且动态地调整后台数据通信的算法——当网速流畅时就使用UDP协议进行数据传输而当网速缓慢时就使用TCP协议进行数据传输。 4网络字节序为大端 对于数据的存储计算机中有大小端的概念 大端模式 数据的高字节内容保存在内存的低地址处数据的低字节内容保存在内存的高地址处。小端模式 数据的高字节内容保存在内存的高地址处数据的低字节内容保存在内存的低地址处。 内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端和小端之分, 网络数据流同样有大端和小端之分。 在只在本地机器上运行的程序无须考虑大小端问题的因为同一台机器上的数据采用的存储方式都是一样的无论采用的是大端存储模式还是小端存储模式数据的读写结果均相同。 但网络通信的程序由于无法保证通信双方存储数据的方式相同因此必须考虑大小端问题否则服务端主机识别的数据可能与客服端发送的数据是不一致的。例如客户端是小端机服务端是大端机客户端通过网络向服务端发送了一个小端地址 0x11223344而在服务端上就被识别成了大端地址 0x44332211。 TCP/IP 协议规定网络数据流均采用大端字节序即低地址高字节无论是大端机还是小端机都必须按照 TCP/IP 协议规定的网络字节序来发送和接收数据。 对于客户端 若客户端是小端则先将数据转成大端再发送到网络当中。若客户端是大端则可以直接进行发送。 而对于服务端 若服务端是小端则先将接收到数据转成大端再进行数据识别。若服务端是大端则可以直接进行数据识别。 例如客户端是小端机服务端是大端机客户端通过网络向服务端发送了一个小端地址 0x11223344这个小端地址按TCP/IP 协议要求会先被转换成大端地址使服务端能够直接进行数据识别服务端上也能识别出 0x11223344。 【补】网络字节序与主机字节序的转换接口         为使网络程序具有可移植性使同样的C代码在大端和小端计算机上编译后都能正常运行系统提供了四个接口以实现网络字节序和主机字节序之间的转换。 #include arpa/inet.h // 主机序列转网络序列 uint32_t htonl(uint32_t hostlong); // 将主机上unsigned int类型的数据转换成对应网络字节序 uint16_t htons(uint16_t hostshort); // 将主机上unsigned short类型的数据转换成对应网络字节序// 网络序列转主机序列 uint32_t ntohl(uint32_t netlong); // 将从网络中读取的unsigned int类型的数据转换成当前计算机字节序 uint16_t ntohs(uint16_t netshort); // 将从网络中读取的unsigned short类型的数据转换成当前计算机字节序 函数名中h 表示 hostn 表示 networkl 表示 32 位长整数s 表示1 6 位短整数。如果主机是小端字节序则这些函数会先将参数做相应的大小端转换然后返回。如果主机是大端字节序则这些函数将不做任何转换将参数原封不动地返回。 【补】为什么网络字节序采用的是大端         主机字节序一般采用的是小端网络字节序采用的却是大端。         如果网络字节序采用小端的话数据在传输过程中就不用进行大小端的转换了但为什么不采用小端而偏偏采用大端呢         对此有一种说法是TCP 在 Unix 时代就有了曾经的 Unix 机器都是大端机因此网络字节序也就采用的是大端但之后人们发现用小端能简化硬件设计于是现在主流的机器都是小端机然而协议已经不好再改了。         而另一种说法是大端序更符合现代人的读写习惯因此网络字节序采用大端。 二、Socket 系列接口 1TCP/UDP 通用接口 .1- 创建套接字 socket() #includesys/types.h #includesys/socket.h int socket(int domain, int type, int protocol); 功能创建套接字并在创建时指定一种通信协议以使用。 参数1.domain创建套接字的域或者叫做协议家族也就是创建套接字的类型。该参数就相当于struct sockaddr结构的前16个位。如果是本地通信就设置为AF_UNIX如果是网络通信就设置为AF_INETIPv4或AF_INET6IPv6。2.type创建套接字时所需的服务类型。例如面向字节流的网络通信 SOCK_STREAMTCP 协议、面向用户数据报的网络通信 SOCK_DGRAM UDP 协议等。3.protocol用于指定具体的协议名的比如指定 TCP 或者 UDP但根据前两个参数其实已经可以确定使用的协议因此一般设置为 0 即可。 返回值创建成功返回一个int类型的值其实就是一个文件描述符失败返回-1并设置合适的错误码。 网络协议栈是分层的由 TCP/IP 分层模型自顶向下依次是应用层、传输层、网络层和数据链路层。 一般程序员所写的代码都是用户级代码也就是说编写代码一般在应用层因此调用的接口实际是属于下三层的。而下三层中的传输层和网络层的操作都是在操作系统内完成的也就意味着在应用层调用的接口其实都叫做系统调用接口。  socket() 是被进程所调用的。socket() 会被写在一个程序中的编码中当程序编码形成的可执行程序也就是进程且被 CPU 调度并执行到 socket() 时会执行创建套接字的代码也就是说socket() 其实是被进程所调用的。 每一个进程在执行时不仅会打开执行相关的文件还会在系统层面上拥有一个进程控制块 task_struct。在 task_struct 中有一个指针指向一个名为 files_struct 的结构体而在 files_struct 结构体中又有一个名为 fd_array 、类型为 struct file* 的指针数组也就是文件描述符表。文件描述符的下标其实就是文件描述符而表中的0、1、2 下标默认依次对应了标准输入流、标准输出流、标准错误流。 当调用 socket() 创建套接字时其实相当于打开了一个“网络文件”并在内核层面上就形成了一个对应的 struct file 结构体。该结构体的首地址会被填入到 fd_array 数组当中下标为 3 的位置上同时被链入到调用 socket() 进程的文件双链表中最终3 号文件描述符就作为 socket() 的返回值被返回了。 每一个 struct file 结构体中包含了进程所打开的文件的各种信息例如文件的属性、操作方法、文件缓冲区等。在内核中文件的属性是由 struct inode 结构体来维护的而文件的操作方法其实是一堆函数指针read*、write*等由 struct file_operations 结构体来维护。 文件缓冲区对于进程打开的普通文件来说一般是磁盘但对于打开的“网络文件”来说其实就是网卡。  .2- 绑定端口号 bind() #includesys/types.h #includesys/socket.h int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 功能在创建好套接字后将套接字与网络绑定关联起来 参数1. sockfd待绑定的文件描述符使用的其实是 socket() 返回的文件描述符。2.addr输出型参数是 struct sockaddr 结构体类型的指针表示网络相关的属性信息包括协议家族、IP地址、端口号等。3.addrlen输入输出型参数是 socklen_t 类型的变量本质是 unsigned int 类型的32位变量用于表示 sockaddr 结构体大小的单位是字节。 返回值绑定成功返回 0失败返回 -1并设置合适的错误码。光创建好套接字也只是在系统层面上打开了一个文件接下来还需要将文件与网络绑定起来让操作系统知道要将数据刷新到网卡而非磁盘。  在使用 bind() 进行绑定时还需要将网络相关的属性信息填充到一个结构体也就是sockaddr_in 中然后 将sockaddr_in* 强转为 sockaddr*。就可以完成对 bind() 第二个参数的传参了。 sockaddr_in 属于系统级概念是一种网络套接字其中封装了许多字段。 //网络套接字 struct sockaddr_in {short int sin_family; // 地址族一般为AF_INETunsigned short int sin_port; // 端口号是一个16位的整数struct in_addr sin_addr; // IP地址是一个32位的整数//... 剩下的字段一般不做处理 }; 在绑定时将 sockaddr_in 进行传参也就相当于将 IP 地址和端口号告诉相应的网络文件之后就可以将网络文件中操作方法的指向因为其本质是函数指针改为对应网卡的操作方法使得读写数据的操作对象为网卡进而做到将文件和网络绑定起来。 2UDP 专用接口 读取数据 recvfrom() #includesys/type.h #includesys/socket.h ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); 功能用于 UDP 协议服务端读取 UDP 协议客户端发送的数据或客户端读取服务端响应的数据 参数1.sockfd指定待读取数据的文件描述符。2.buf指定读取数据的存储位置。3.len指定读取数据的字节数。4.flags指定数据的读取方式一般设置为0阻塞读取。5.src_addr是一个结构体表示对端网络相关的属性信息包括协议家族、IP地址、端口号等。6.addrlen输入输出型参数传参时表示 src_addr 的长度返回时表示读取到的 src_addr 的长度。 返回值读取成功则返回实际读取到的字节数失败则返回-1并设置合适的错误码。 【ps】 1.由于UDP是不面向连接的因此除了要获取数据还要获取对端网络相关的属性信息包括 IP 地址、端口号等。 2.由于 recvfrom() 提供的参数也是struct sockaddr*类型因此在传参时需要先将 struct sockaddr_in* 强转为 struct sockaddr*。 发送数据 sendto() #includesys/type.h #includesys/socket.h ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); 功能用于 UDP 协议客户端向 UDP 协议服务端发送写入数据或服务端向客户端响应数据 参数1.sockfd待写入数据的文件的文件描述符。2.buf待写入数据。3.len指定写入数据的字节数。4.flags指定数据的写入方式一般设置为0表示阻塞写入。5.dest_addr对端网络相关的属性信息包括协议家族、IP地址、端口号等。6.addrlen表明 dest_addr 结构体的长度。 返回值写入成功则返回实际写入的字节数失败返回-1并设置合适的错误码。 【ps】 1.由于UDP是不面向连接的因此除了要获取数据还要获取对端网络相关的属性信息包括 IP 地址、端口号等。 2.由于 sendto() 提供的参数 dest_addr 也是struct sockaddr*类型因此在传参时需要先将 struct sockaddr_in* 强转为 struct sockaddr*。3TCP 专用接口 监听套接字 listen() #includesys/type.h #includesys/socket.h int listen(int sockfd, int backlog); 功能用于在 bind() 之后服务端监听客户端的连接请求。 参数1.sockfd 需要设置为监听状态的套接字文件描述符。2.backlog全连接队列的最大长度。如果有多个客户端同时发来连接请求未被服务端处理的请求会放入连接队列该参数代表的就是全连接队列的最大长度一般设置为5或10即可。 返回值监听成功返回 0失败返回-1并设置合适的错误码。 接收连接请求 accept() #includesys/type.h #includesys/socket.h int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 功能用于在 listen() 之后TCP协议的服务端接收客户端的连接请求 参数1.sockfd特定的监听套接字表明会从该监听套接字中获取连接请求。2.addr输出型参数对端主机的网络信息包括协议家族、IP地址、端口号等。3.addrlen输入输出型参数传参时表示待读取的addr结构体的长度返回时表示实际读取到的addr结构体的长度 返回值接收成功返回接收到的客户端的套接字失败返回-1并设置合适的错误码。 【ps】 1.用于为accept()获取到的连接请求提供服务的其实是accept()返回的套接字。而作为accept()参数的监听套接字只负责不断获取新的连接请求。 2.accept()在获取连接时可能会失败但TCP服务端不会因为这种失败而退出因此在accept()获取失败后应该继续获取。 3.如果要将获取的连接请求所对应的客户端其中的IP地址和端口号信息进行输出需要调用inet_ntoa()将整数IP转换成字符串IP、调用ntohs()将端口号由网络序列转换成主机序列。【补】socket() 与 accept() 中的文件描述符         最初由 socket() 创建的描述符用于服务端接收一台客户端的连接请求不用于与客户端的通讯。而 accept() 返回的文件描述符用于服务端与某一台客户端的通讯。         比方说在一家餐厅中有正在店外接客的服务员也有正在店内服务的服务员而socket() 创建的描述符就好比在店外接客的服务员accept() 返回的描述符就好比在店内服务的服务员。         负责店外接客的服务员在接引完客人之后会回到店门口继续接客在接客的过程中哪怕只有一个服务员也基本可以完成接客工作而店内可能同时有多桌客人要用餐于是就要有多名在店内服务的服务员来完成服务工作且为了保证服务的可靠性一名服务员以此只能服务一桌客人直到这一桌客人离店才清理餐桌然后等待下一桌客人光临。         类似的 socket() 创建的描述符在接收了客户端的连接请求后会继续等待下一台客户端的连接请求送达而全程只有一个这样的描述符accept() 返回的文件描述符负责服务端与某一台客户端的通讯在通讯结束之后会被关闭等待下一台要通讯的客户端。 数据请求相关 read()、write() #includesys/type.h #includesys/socket.h ssize_t read(int fd, void *buf, size_t count); 功能从accept()返回的套接字中读取数据用于服务端读取客户端发来的数据、客户端向服务端回应数据。 参数1.fd待读取的文件描述符。2.buf将读取到的数据存储到 buf 所指的位置。3.count从 fd 中读取数据的字节数。 返回值返回值 0 时表示实际读取到的字节数。返回值 0 时表示对端客户端已关闭连接服务端不再为其提供服务。返回值 0 时表示读取失败并设置合适的错误码。 【ps】返回值为 0 表示对端已关闭连接类似于本地进程间的管道通信————· 写端不写会使数据未就绪读端进程一直读就会被挂起· 读端不读写端一直写待管道写满后就会被挂起· 写端写完后将写端关闭读端就会读到 0· 读端将读端关闭写端写入的数据不会被读取写端就会被系统杀掉。发来连接请求的客户端好比写端接收连接请求的服务端好比读端客户端关闭连接服务端就不必再为其提供服务了对于 read() 返回值为 0。#includesys/type.h #includesys/socket.h ssize_t write(int fd, const void *buf, size_t count); 功能向accept()返回的套接字中写入数据用于服务端向客户端回应数据、客户端向服务端发送数据。 参数1.fd待写入数据的文件描述符。2.buf指向待写入数据的指针。3.count指定待写入数据的字节数。 返回值写入成功则返回实际写入的字节数失败则返回-1并设置合适的错误码。 建立连接 connect #includesys/type.h #includesys/socket.h int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 功能用于客户端与服务端建立连接。 参数1.sockfd发起连接请求的文件描述符。2.addr输出型参数对端主机的网络信息包括协议家族、IP地址、端口号等。。3.addrlen输入输出型参数即 addr 的大小。 返回值连接成功返回 0失败返回 -1 并设置合适的错误码。 4sockaddr 结构体         socket 其实有很多种类型常见的有以下三种 网络套接字用户跨主机之间的通信也能支持本地通信。原始套接字可以跨过传输层(TCP/UDP)访问底层的数据。域间套接字只能在本地通信。 它们的应用场景完全不同因此不同种类的 socket 都各自对应了一套系统调用接口也就是说三套 socket 就会对应三套不同的接口。 socket 不仅支持跨网络的进程间通信还支持本地的进程间通信这是因为 socket 本身提供了支持跨网络通信的 sockaddr_in 结构体和支持本地通信的 sockaddr_un 结构体。 //网络套接字 struct sockaddr_in {short int sin_family; // 地址族一般为AF_INETunsigned short int sin_port; // 端口号网络字节序struct in_addr sin_addr; // IP地址unsigned char sin_zero[8]; // 用于填充使sizeof(sockaddr_in)等于16 }; //域间套接字 struct sockaddr_un {sa_family_t sun_family; /* AF_UNIX */char sun_path[108]; /* 带有路径的文件名 */ }; 为了在进行 socket 的网络通信和本地通信时能够使用同一套函数接口于是就出现了sockeaddr 结构体虽然该结构体与 sockaddr_in 和 sockaddr_un 的结构都不相同但这三个结构体头部的 16 个比特位都是相同的而这 16 个比特位的字段叫做协议家族。 因此在对相关的 socket 系统接口进行传参时无须分情况传入 sockaddr_in 或 sockaddr_un只需统一传入 sockeaddr 即可且通过设置协议家族这个字段就可以表面当前是要进行网络通信还是本地通信。 总得来说可以将 sockaddr 看成是基类把 sockaddr_in 和 sockaddr_un 看成是派生类。 不过进行网络通信的编程时还是会定义 sockaddr_in 这样的结构体只是需要在传参时将 sockaddr_in* 强转为 sockaddr* 。 通过 sockaddr_in 结构体将IP地址端口号以及网络通信AF_INET通过系统调用bind与系统绑定从而进行网络通信。但在使用 sockaddr_in 结构体之前需先用 bzero() 或 memset() 将其清零 #include strings.h void bzero(void *s, size_t n); 功能只能清零 参数1.s待清零的地址2.n待清零的字节数 然后将 sockaddr_in 结构体的地址类型成员 sin_family协议簇填充为 AF_INET 以表明要进行网络通信在填充 sin_port端口号时需要先使用 htons()将主机字节序转换成网络字节序然后再进行填充而在填充 sin_addrIP地址时需用到 inet_addr()。 5IP 地址的表现形式 IP地址的表现形式有字符串和整数两种。 字符串 IP或称基于字符串的点分十进制 IP 地址例如 192.168.233.123一个地址有 15 字节。整数 IP直接用一个 32 位的整数来表示 IP 地址一个地址有 4 字节。 在网络通信时直接使用字符串 IP那么传输一个 IP 地址至少就需要 15 个字节但实际并不需要耗费这么多空间。 这是因为字符串 IP 可以划分为四个区域其中每一个区域的取值都是 0~255而 0~255这个范围只需要用 8 个比特位就能表示因此 32 个比特位其实就能够表示一个 IP 地址。这个 32位其实就是一个整数整数每一个字节都对应了字符串 IP 中的某个区域而 这这种表示 IP 地址的方法就称为整数 IP此时只需要 4 个字节就可以传输一个 IP 地址。 整数 IP 和字符串 IP 都能表示同样的含义而整数 IP 在传输时的数据量更小因此在网络通信中 IP 地址的表现形式采用的是整数 IP 。 但字符串 IP 仍在用户之间被广泛使用因此在网络通信时也就一定会涉及字符串 IP 与整数 IP 之间的相互转换。 在操作系统内部字符串 IP 与整数 IP 之间的相互转换是通过位段和枚举来实现的。 由于联合体的空间是成员共享的因此设置IP和读取IP的方式可以如下 要以整数 IP 的形式设置 IP 时只需直接将其赋值给联合体的第一个成员。要以字符串 IP 的形式设置 IP 时只需先将字符串 IP 分成对应的四部分然后将每部分转换成对应的二进制序列并依次设置到联合体中第二个成员当中的p1、p2、p3、p4。要取出整数 IP 时只需直接读取联合体的第一个成员。要取出字符串 IP 时只需依次获取联合体第二个成员中的p1、p2、p3、p4然后将每一部分转换成字符串且拼接到一起。 而操作系统向上层提供了一些接口以支持网络编程中字符串 IP 与整数 IP 之间的相互转换。 #include sys/socket.h #include netinet/in.h #include arpa/inet.h in_addr_t inet_addr(const char *cp); 功能将字符串 IP 转换成整数 IP 参数待转换的字符串 IP 返回值转换后的整数 IP #include sys/socket.h #include netinet/in.h #include arpa/inet.h char *inet_ntoa(struct in_addr in); 功能整数 IP 转换成字符串 IP 参数待转换的整数 IP实际是一个封装了字符串 IP 四个部分的结构体 返回值转换后的字符串 IP 三、基于 UDP 协议的网络编程 网络通信是双向的一端是接收数据的服务端Server另一端是发送数据的客户端Client。 1服务端 .1- 创建套接字 udp_server.hpp class UdpServer { public:UdpServer(std::string ip, int port):_sockfd(-1)//...{};bool InitServer(){//创建套接字//使用 IPv4 的协议簇、使用 UDP 协议_sockfd socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd 0){ std::cerr socket error std::endl;//创建失败打印提示信息return false;}//创建成功打印文件描述符 socket()的返回值 std::cout socket create success, sockfd: _sockfd std::endl;//...return true;}//...~UdpServer(){ //对于合法的文件描述符在析构时关闭其所对应的文件if (_sockfd 0){close(_sockfd);}}; private:int _sockfd; //文件描述符//... };udp_server.cc int main() {UdpServer* svr new UdpServer();svr-InitServer();return 0; }进程成功创建套接字后获取到的文件描述符就是 3这是因为 0、1、2  已经被标准输入流、标准输出流、标准错误流占用了当前未被利用且最小的文件描述符就是 3。 .2- 绑定套接字 udp_server.hpp class UdpServer { public:UdpServer(std::string ip, int port):_sockfd(-1),_port(port),_ip(ip){};bool InitServer(){//1.创建套接字_sockfd socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd 0){ //创建套接字失败std::cerr socket error std::endl;return false;}std::cout socket create success, sockfd: _sockfd std::endl;//2.绑定套接字//①先填充网络通信的相关信息struct sockaddr_in local; //创建 sockaddr_in 对象memset(local, \0, sizeof(local)); //清零local.sin_family AF_INET; //设置 sin_family 字段local.sin_port htons(_port); //用 htons() 设置 sin_port 字段local.sin_addr.s_addr inet_addr(_ip.c_str()); //用 inet_addr() 设置 s_addr 字段 //②开始绑定if (bind(_sockfd, (struct sockaddr*)local, sizeof(sockaddr)) 0){ std::cerr bind error std::endl;//绑定失败则打印提示信息return false;}std::cout bind success std::endl; //绑定成功也打印提示信息方便调试return true;}//...~UdpServer(){if (_sockfd 0){close(_sockfd);}}; private:int _sockfd; //文件描述符int _port; //端口号std::string _ip; //IP地址 }; .3- 运行 创建和绑定好套接字服务端的初始化也就完成了接下来就可以运行服务端了。         服务端之所以称为服务端是因为它在运行之后永不退出会周而复始地提供某种服务因此服务端其实执行的是一个死循环代码。 由于 UDP 协议是不面向连接的因此在 UDP 服务端启动后就可以直接读取客户端发来的数据。 udp_server.hpp #define SIZE 128class UdpServer { public:UdpServer(std::string ip, int port):_sockfd(-1),_port(port),_ip(ip){};bool InitServer(){//1.创建套接字_sockfd socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd 0){ //创建套接字失败std::cerr socket error std::endl;return false;}std::cout socket create success, sockfd: _sockfd std::endl;//2.绑定套接字//①先填充网络通信的相关信息struct sockaddr_in local; //创建 sockaddr_in 对象memset(local, \0, sizeof(local)); //清零local.sin_family AF_INET; //设置 sin_family 字段local.sin_port htons(_port); //用 htons() 设置 sin_port 字段local.sin_addr.s_addr inet_addr(_ip.c_str()); //用 inet_addr() 设置 s_addr 字段 //②开始绑定if (bind(_sockfd, (struct sockaddr*)local, sizeof(sockaddr)) 0){ std::cerr bind error std::endl;//绑定失败则打印提示信息return false;}std::cout bind success std::endl; //绑定成功也打印提示信息方便调试return true;}//3.运行服务端void Start(){char buffer[SIZE]; //用于存放对端发送的数据while(1){struct sockaddr_in peer;//用于保存对端的网络信息socklen_t len sizeof(peer);//服务端调用 recvfrom() 以读取客户端发送的数据ssize_t size recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)peer, len);//读取成功则打印对端主机的IP地址和端口号if (size 0){buffer[size] \0;int port ntohs(peer.sin_port); //ntohs()可以将sockaddr_in的sin_port转化为人看得懂的端口号std::string ip inet_ntoa(peer.sin_addr);//inet_ntoa()可以将整数IP转换成字符串IP//打印对端主机的IP地址和端口号std::cout ip : port # buffer std::endl;}//读取失败则打印提示信息但服务端不会退出else{std::cerr recvfrom error std::endl;}//只要收到数据就向客户端响应std::string echo_msg server get!-;echo_msg buffer;sendto(_sockfd, echo_msg.c_str(), echo_msg.size(), 0, (struct sockaddr*)peer, len);}}~UdpServer(){if (_sockfd 0){close(_sockfd);}};private:int _sockfd; //文件描述符int _port; //端口号std::string _ip; //IP地址 }; udp_server.cc int main(int argc, char* argv[]) //引入命令行参数在服务端在运行时可以跟上IP地址和端口号 {//错误处理if (argc ! 2){std::cerr Usage: argv[0] port std::endl;return 1;}//由云服务器的特殊性服务端运行时其实无需跟上IP地址仅跟端口号即可//这里手动将IP地址设置为127.0.0.1等价于localhost表示本地主机或称本地环回这样方便测试std::string ip 127.0.0.1; //单独获取端口号int port atoi(argv[1]);//开始运行UdpServer* svr new UdpServer(ip, port);svr-InitServer();svr-Start();return 0; }运行效果         指令 netstat -nlup 可以查看对应网络相关的信息。         指令 netstat -lup 相比于指令 netstat -nlup 用对应的域名服务器替换原本显示的IP地址。 2客户端 【Tips】绑定的细节服务端需绑定套接字而客户端无需绑定套接字。        为了能在网络通信时找到通信的彼此服务端和客户端都需要有各自的 IP 地址和端口号只不过服务端需要绑定套接字客户端则不需要。         为了能够接受服务端提供的服务客户端就需要知道服务端的 IP 地址和端口号。通常来说IP 地址对应的是域名但端口号并没有显示指明过因此服务端的端口号一定要是一个众所周知的端口号且在选定后不能轻易改变换句话说服务端需要进行绑定经绑定之后的端口号才真正属于自己。一个端口号只能被一个进程所绑定服务端进行了绑定就独占了这个端口。         客户端虽然也需要端口号但一般并不需要进行绑定只需在访问服务端时客户端的端口号是唯一的即可无需与特定的客户端进程强相关。如果客户端绑定了某个端口号那么这个端口号以后就只能给这一个客户端使用而如果这个客户端没有启动这个端口号也就无法分配给别人另外如果这个端口号被别人使用了这个客户端也就无法启动了。因此客户端的端口号只需要保证唯一性而无需进行绑定这样客户端的端口号可以动态地进行设置一般是调用类似于 sendto() 这样的接口操作系统会自动给当前客户端分配一个唯一的端口号。也就是说客户端每次启动时使用的端口号可能是不同的且只要系统中的端口号没有被耗尽客户端就永远可以启动。 .1- 创建套接字 udp_client.hpp class UdpClient { public:UdpClient(std::string server_ip, int server_port):_sockfd(-1),_server_port(server_port),_server_ip(server_ip){}bool InitClient(){//创建套接字//使用 IPv4 的协议簇、使用 UDP 协议_sockfd socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd 0){std::cerr socket create error std::endl;return false;}return true;}//...~UdpClient(){if (_sockfd 0){close(_sockfd);}} private:int _sockfd; //文件描述符int _server_port; //服务端端口号std::string _server_ip; //服务端IP地址 }; .2- 运行 udp_client.hpp #define SIZE 128class UdpClient { public:UdpClient(std::string server_ip, int server_port):_sockfd(-1),_server_port(server_port),_server_ip(server_ip){}//1.创建套接字bool InitClient(){//使用 IPv4 的协议簇、使用 UDP 协议_sockfd socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd 0){std::cerr socket create error std::endl;return false;}return true;}//2.运行客户端void Start(){std::string msg; //用于存储待发送的数据struct sockaddr_in peer; //用于保存对端主机的网络信息memset(peer, \0, sizeof(peer));peer.sin_family AF_INET;peer.sin_port htons(_server_port);peer.sin_addr.s_addr inet_addr(_server_ip.c_str());while(1){std::cout Please Enter# ;//从键盘获取数据getline(std::cin, msg); //向客户端发送数据sendto(_sockfd, msg.c_str(), msg.size(), 0, (struct sockaddr*)peer, sizeof(peer)); char buffer[SIZE];struct sockaddr_in tmp;socklen_t len sizeof(tmp);ssize_t size recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)tmp, len);if (size 0){buffer[size] \0;std::cout buffer std::endl;}}}~UdpClient(){if (_sockfd 0){close(_sockfd);}} private:int _sockfd; //文件描述符int _server_port; //服务端端口号std::string _server_ip; //服务端IP地址 }; udp_client.cc int main(int argc, char* argv[]) {//错误处理if (argc ! 3){std::cerr Usage: argv[0] server_ip server_port std::endl;return 1;}//获取输入的IP地址和端口号std::string server_ip argv[1];int server_port atoi(argv[2]);//开始运行UdpClient* clt new UdpClient(server_ip, server_port);clt-InitClient();clt-Start();return 0; }Makefile  .PHONY:all all:udp_client udp_serverudp_client:udp_client.ccg -o $ $^ -stdc11 udp_server:udp_server.ccg -o $ $^ -stdc11.PHONY:clean clean:rm -f udp_client udp_server 3本地测试 当前服务端并没有绑定外网而绑定的是本地环回 127.0.0.1以便进行本地测试。         运行服务端时只需指明端口号即可这里为 8080 。         运行客户端时需指明 IP 地址为本地环回 127.0.0.1、服务端的端口号 8080。         输入指令 netstat -nlup 可以查看当前的查看网络信息。 4网络测试 .1- 绑定INADDR_ANY 要进行网络测试就要让服务端绑定云服务器的公网 IP这里小编的云服务器的公网 IP 为122.152.220.113。 但由于云服务器的 IP 地址是由云厂商提供的这个 IP 地址并不一定是真正的公网IP因此是不能直接被绑定的如果要让外网访问就需要在设置 bind() 的参数即填充 sockaddr_in 结构体时将 sin_addr 字段设置为 INADDR_ANY 。 INADDR_ANY 本身是一个宏值对应的值为 0因此也不存在大小端的问题在设置时可以不考虑网络字节序的转换。 【补】绑定 INADDR_ANY 的好处         当一台机器的带宽足够大时它接收数据的能力同时也会约束它的 IO 效率因此为了提升 IO 效率一台机器底层可能装有多张网卡同时可能会使它拥有有多个IP地址。         在一台机器的服务端上端口号为 8080 的服务只有一个但这个服务端在底层可能有多张与它相关的网卡因此它在接收数据时与它相关的多张网卡会都收到数据。         如果服务端在绑定时是指明绑定的某一个具体的 IP 地址也就是绑定了一张特定的网卡那么服务端在接收客户端发来的数据时就只能从特定的一张网卡中读取数据。         但如果服务端绑定的是 INADDR_ANY那么只要是发送给端口号为 8080 的服务的数据都可以被系统自底向上交给该服务端。 udp_server.hpp #define SIZE 128class UdpServer { public:UdpServer(std::string ip, int port):_sockfd(-1),_port(port),_ip(ip){};bool InitServer(){_sockfd socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd 0){ std::cerr socket error std::endl;return false;}std::cout socket create success, sockfd: _sockfd std::endl;//绑定套接字struct sockaddr_in local; memset(local, \0, sizeof(local)); local.sin_family AF_INET; local.sin_port htons(_port); local.sin_addr.s_addr INADDR_ANY; //绑定INADDR_ANY //②开始绑定if (bind(_sockfd, (struct sockaddr*)local, sizeof(sockaddr)) 0){ std::cerr bind error std::endl;return false;}std::cout bind success std::endl; return true;}void Start(){char buffer[SIZE]; while(1){struct sockaddr_in peer;socklen_t len sizeof(peer);ssize_t size recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)peer, len);if (size 0){buffer[size] \0;int port ntohs(peer.sin_port); std::string ip inet_ntoa(peer.sin_addr);std::cout ip : port # buffer std::endl;}else{std::cerr recvfrom error std::endl;}std::string echo_msg server get!-;echo_msg buffer;sendto(_sockfd, echo_msg.c_str(), echo_msg.size(), 0, (struct sockaddr*)peer, len);}}~UdpServer(){if (_sockfd 0){close(_sockfd);}};private:int _sockfd; int _port; std::string _ip; }; 用指令 netstat -nlup 查看网络信息后发现服务端的本地 IP 地址从原本 127.0.0.1 变成了 0.0.0.0这就意味着该 UDP 服务端现在可以在本地读取任何一张网卡的数据。  接下来就可以进行网络测试了。 .2- 测试效果 先启动服务端再将服务端的IP地址和端口号作为命令行参数去启动客户端此时客户端和服务端就可以进行网络通信了。 【补】分发客户端         使用指令 sz 将客户端可执行程序下载到本地机器然后发送给其他人。         其他人收到客户端可执行程序后可以通过指令 rz 或拖拽的方式将其上传到自己的云服务器上然后通过指令 chmod 命令为可执行程序加上可执行权限后即可正常运行。 注小编云服务器的系统是 CentOS 7 版本的 Linux 5完整代码 //udp_server.hpp #include iostream #include cstdio #include string #include cerrno #include cstring #include cstdlib #include strings.h #include sys/socket.h #includesys/types.h #includesys/stat.h #includefcntl.h #includeunistd.h #include netinet/in.h #include arpa/inet.h#define SIZE 128class UdpServer { public:UdpServer(std::string ip, int port):_sockfd(-1),_port(port),_ip(ip){};bool InitServer(){_sockfd socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd 0){ std::cerr socket error std::endl;return false;}std::cout socket create success, sockfd: _sockfd std::endl;struct sockaddr_in local;memset(local, \0, sizeof(local));local.sin_family AF_INET;local.sin_port htons(_port);local.sin_addr.s_addr INADDR_ANY; if (bind(_sockfd, (struct sockaddr*)local, sizeof(sockaddr)) 0){ std::cerr bind error std::endl;return false;}std::cout bind success std::endl;return true;}void Start(){char buffer[SIZE];while(1){struct sockaddr_in peer;socklen_t len sizeof(peer);ssize_t size recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)peer, len);if (size 0){buffer[size] \0;int port ntohs(peer.sin_port);std::string ip inet_ntoa(peer.sin_addr);std::cout ip : port # buffer std::endl;}else{std::cerr recvfrom error std::endl;}std::string echo_msg server get!-;echo_msg buffer;sendto(_sockfd, echo_msg.c_str(), echo_msg.size(), 0, (struct sockaddr*)peer, len);}}~UdpServer(){if (_sockfd 0){close(_sockfd);}}; private:int _sockfd; int _port0; std::string _ip; }; //udp_client.hpp #include iostream #include cstdio #include string #include cerrno #include cstring #include cstdlib #include strings.h #include sys/socket.h #includesys/types.h #includesys/stat.h #includefcntl.h #includeunistd.h #include netinet/in.h #include arpa/inet.h#define SIZE 128class UdpClient { public:UdpClient(std::string server_ip, int server_port):_sockfd(-1),_server_port(server_port),_server_ip(server_ip){}bool InitClient(){_sockfd socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd 0){std::cerr socket create error std::endl;return false;}return true;}void Start(){std::string msg; struct sockaddr_in peer; memset(peer, \0, sizeof(peer));peer.sin_family AF_INET;peer.sin_port htons(_server_port);peer.sin_addr.s_addr inet_addr(_server_ip.c_str());while(1){std::cout Please Enter# ;getline(std::cin, msg); sendto(_sockfd, msg.c_str(), msg.size(), 0, (struct sockaddr*)peer, sizeof(peer)); char buffer[SIZE];struct sockaddr_in tmp;socklen_t len sizeof(tmp);ssize_t size recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)tmp, len);if (size 0){buffer[size] \0;std::cout buffer std::endl;}}}~UdpClient(){if (_sockfd 0){close(_sockfd);}} private:int _sockfd; int _server_port; std::string _server_ip; }; //udp_server.cc #include udp_server.hpp #include memory #include cstdlibint main(int argc, char* argv[]) {if (argc ! 2){std::cerr Usage: argv[0] port std::endl;return 1;}std::string ip 127.0.0.1; int port atoi(argv[1]);UdpServer* svr new UdpServer(ip, port);svr-InitServer();svr-Start();return 0; } //udp_client.cc #include udp_client.hpp #include memory #include cstdlib int main(int argc, char* argv[]) {if (argc ! 3){std::cerr Usage: argv[0] server_ip server_port std::endl;return 1;}std::string server_ip argv[1];int server_port atoi(argv[2]);UdpClient* clt new UdpClient(server_ip, server_port);clt-InitClient();clt-Start();return 0; } //Makefile .PHONY:all all:udp_client udp_serverudp_client:udp_client.ccg -o $ $^ -stdc11 udp_server:udp_server.ccg -o $ $^ -stdc11.PHONY:clean clean:rm -f udp_client udp_server 四、基于 TCP 协议的网络编程 1服务端 .1- 创建套接字 tcp_server.hpp class TcpServer { public:TcpServer(): _sock(-1)//...{}void InitServer(){//创建套接字//使用 IPv4 的协议簇、使用 TCP 协议_sock socket(AF_INET, SOCK_STREAM, 0);if (_sock 0){std::cerr socket error std::endl;exit(2);}//...}//...~TcpServer(){if (_sock 0){close(_sock);}} private:int _sock; //文件描述符//... };.2- 绑定 tcp_server.hpp class TcpServer { public:TcpServer(int port): _sock(-1), _port(port){}void InitServer(){//1.创建套接字_sock socket(AF_INET, SOCK_STREAM, 0);if (_sock 0){std::cerr socket error std::endl;exit(2);}//2.绑定//①先填充网络通信的相关信息struct sockaddr_in local;memset(local, \0, sizeof(local));local.sin_family AF_INET; //表明进行网络通信local.sin_port htons(_port); //用 htons() 设置端口号local.sin_addr.s_addr INADDR_ANY; //IP 地址绑定为 INADDR_ANY//②开始绑定if (bind(_sock, (struct sockaddr*)local, sizeof(local)) 0){std::cerr bind error std::endl;exit(3);}}//...~TcpServer(){if (_sock 0){close(_sock);}} private:int _sock; //文件描述符int _port; //端口号 };.3- 监听 TCP 服务端在创建和绑定好套接字后还需要将套接字设置为监听状态以监听是否有新的客户端数据请求到来。 如果监听失败也就意味着 TCP 服务端无法接收客户端发来的数据请求此时就直接终止服务端程序即可。 tcp_server.hpp #define BACKLOG 5class TcpServer { public:TcpServer(int port): _listen_sock(-1), _port(port){}void InitServer(){//1.创建套接字_listen_sock socket(AF_INET, SOCK_STREAM, 0);if (_listen_sock 0){std::cerr socket error std::endl;exit(2);}//2.绑定struct sockaddr_in local;memset(local, \0, sizeof(local));local.sin_family AF_INET;local.sin_port htons(_port);local.sin_addr.s_addr INADDR_ANY;if (bind(_listen_sock, (struct sockaddr*)local, sizeof(local)) 0){std::cerr bind error std::endl;exit(3);}//3.监听if (listen(_listen_sock, BACKLOG) 0){std::cerr listen error std::endl;exit(4);}}//...~TcpServer(){if (_sock 0){close(_sock);}} private:int _listen_sock; //待监听的套接字int _port; //端口号 };.4- 获取数据请求 服务端进入监听状态后如果有客户端发来数据请求就要对其进行获取操作。 tcp_server.hpp #define BACKLOG 5class TcpServer { public:TcpServer(int port): _listen_sock(-1), _port(port){}void InitServer(){//1.创建套接字_listen_sock socket(AF_INET, SOCK_STREAM, 0);if (_listen_sock 0){std::cerr socket error std::endl;exit(2);}//2.绑定struct sockaddr_in local;memset(local, \0, sizeof(local));local.sin_family AF_INET;local.sin_port htons(_port);local.sin_addr.s_addr INADDR_ANY;if (bind(_listen_sock, (struct sockaddr*)local, sizeof(local)) 0){std::cerr bind error std::endl;exit(3);}//3.监听if (listen(_listen_sock, BACKLOG) 0){std::cerr listen error std::endl;exit(4);}}//4.运行void Start(){while(1){//1获取连接struct sockaddr_in peer;memset(peer, \0, sizeof(peer));socklen_t len sizeof(peer);int sock accept(_listen_sock, (struct sockaddr*)peer, len);if (sock 0){std::cerr accept error, continue next std::endl;continue;}std::string client_ip inet_ntoa(peer.sin_addr);int client_port ntohs(peer.sin_port);std::coutget a new link-sock [client_ip]:client_portstd::endl;//...}}private:int _listen_sock; //待监听的套接字int _port; //端口号 };tcp_server.cc void Usage(std::string proc) {std::cout Usage: proc port std::endl; } int main(int argc, char* argv[]) {if (argc ! 2){Usage(argv[0]);exit(1);}int port atoi(argv[1]);TcpServer* svr new TcpServer(port);svr-InitServer();svr-Start();return 0; }运行效果         指令 netstat -nltp 可以查看相关的网络信息。         通过指令 telnet 需先安装安装指令yum -y install telnet telnet-server xinetd向这个 TCP 服务端发送连接请求后可以看到为其提供服务的的文件描述符为 4。         这是因为 0、1、2 已经对应了标准输入流、标准输出流、标准错误流而 3 号文件描述符在初始化服务端时被分配给了监听套接字因此当第一个客户端发起连接请求时为其提供服务的文件描述符为 4。         如果接下来还有其他客户端向这个 TCP 服务端发起连接请求那么此时为其提供服务的文件描述符就为 5。 .5- 处理数据请求 服务端获取到客户端的数据请求后就要对客户端的数据请求进行处理。 但注意此时为客户端提供服务的并不是监听套接字而是 accept() 返回的服务套接字。服务端读取数据是从服务套接字中读取的而写入数据也是写入到服务套接字的体现了 TCP 协议全双工的通信的特点。 tcp_server.hpp #define BACKLOG 5class TcpServer { public:TcpServer(int port): _listen_sock(-1), _port(port){}void InitServer(){//1.创建套接字_listen_sock socket(AF_INET, SOCK_STREAM, 0);if (_listen_sock 0){std::cerr socket error std::endl;exit(2);}//2.绑定struct sockaddr_in local;memset(local, \0, sizeof(local));local.sin_family AF_INET;local.sin_port htons(_port);local.sin_addr.s_addr INADDR_ANY;if (bind(_listen_sock, (struct sockaddr*)local, sizeof(local)) 0){std::cerr bind error std::endl;exit(3);}//3.监听if (listen(_listen_sock, BACKLOG) 0){std::cerr listen error std::endl;exit(4);}}//4.运行void Service();void Start(){while(1){//1获取连接请求struct sockaddr_in peer;memset(peer, \0, sizeof(peer));socklen_t len sizeof(peer);int sock accept(_listen_sock, (struct sockaddr*)peer, len);if (sock 0){std::cerr accept error, continue next std::endl;continue;}std::string client_ip inet_ntoa(peer.sin_addr);int client_port ntohs(peer.sin_port);std::coutget a new link-sock [client_ip]:client_portstd::endl;//2处理连接请求Service(sock, client_ip, client_port);}}void Service(int sock, std::string client_ip, int client_port){char buffer[1024];while (1){ssize_t size read(sock, buffer, sizeof(buffer)-1);//读取成功打印读取的数据并向对端回应数据然后继续读取if (size 0){ buffer[size] \0;std::cout client_ip : client_port # buffer std::endl;write(sock, buffer, size);} //对端关闭连接就不再为其提供服务else if (size 0){ std::cout client_ip : client_port close! std::endl;break;}//读取失败打印提示信息并结束读取else{ std::cerr sock read error! std::endl;break;}}//读取完毕后关闭对应的文件close(sock); std::cout client_ip : client_port service done! std::endl;}private:int _listen_sock; //待监听的套接字int _port; //端口号 };2客户端 【ps】客户端无需进行绑定和监听。 .1- 创建套接字 tcp_client.hpp  class TcpClient { public:TcpClient(std::string server_ip, int server_port): _sock(-1), _server_ip(server_ip), _server_port(server_port){}void InitClient(){//创建套接字_sock socket(AF_INET, SOCK_STREAM, 0);if (_sock 0){std::cerr socket error std::endl;exit(2);}//...}//...~TcpClient(){if (_sock 0){close(_sock);}} private:int _sock; //文件描述符std::string _server_ip; //服务端的IP地址int _server_port; //服务端的端口号//客户端必须要知道它要连接的服务端的IP地址和端口号才能与特定的服务端进行通信 };.2- 连接服务端 tcp_client.hpp  class TcpClient { public:TcpClient(std::string server_ip, int server_port): _sock(-1), _server_ip(server_ip), _server_port(server_port){}void InitClient(){//1.创建套接字_sock socket(AF_INET, SOCK_STREAM, 0);if (_sock 0){std::cerr socket error std::endl;exit(2);}}//2.运行void Start(){//1连接服务端//设置对端服务端的网络信息struct sockaddr_in peer;memset(peer, \0, sizeof(peer));peer.sin_family AF_INET;peer.sin_port htons(_server_port);peer.sin_addr.s_addr inet_addr(_server_ip.c_str());//调用connect()与服务端建立连接//连接成功则向服务端发送数据请求if (connect(_sock, (struct sockaddr*)peer, sizeof(peer)) 0){ //connect successstd::cout connect success... std::endl;//2发送数据请求//...}//连接失败则打印提示信息并退出else{ std::cerr connect failed... std::endl;exit(3);}}~TcpClient(){if (_sock 0){close(_sock);}} private:int _sock; //文件描述符std::string _server_ip; //服务端的IP地址int _server_port; //服务端的端口号//客户端必须要知道它要连接的服务端的IP地址和端口号//才能通过connect()与特定的服务端进行通信 };.3- 发送数据请求 tcp_client.hpp  class TcpClient { public:TcpClient(std::string server_ip, int server_port): _sock(-1), _server_ip(server_ip), _server_port(server_port){}void InitClient(){//1.创建套接字_sock socket(AF_INET, SOCK_STREAM, 0);if (_sock 0){std::cerr socket error std::endl;exit(2);}}//2.运行void Request();void Start(){//1连接服务端//设置对端服务端的网络信息struct sockaddr_in peer;memset(peer, \0, sizeof(peer));peer.sin_family AF_INET;peer.sin_port htons(_server_port);peer.sin_addr.s_addr inet_addr(_server_ip.c_str());//调用connect()与服务端建立连接//连接成功则向服务端发送数据请求if (connect(_sock, (struct sockaddr*)peer, sizeof(peer)) 0){ //connect successstd::cout connect success... std::endl;//2发送数据请求Request();}//连接失败则打印提示信息并退出else{ std::cerr connect failed... std::endl;exit(3);}}void Request(){std::string msg;char buffer[1024];while (1){std::cout Please Enter# ;getline(std::cin, msg);//向文件描述符/套接字中写入数据以发送给服务端write(_sock, msg.c_str(), msg.size());//读取服务端的响应数据ssize_t size read(_sock, buffer, sizeof(buffer)-1);if (size 0){buffer[size] \0;std::cout server echo# buffer std::endl;}else if (size 0){std::cout server close! std::endl;break;}else{std::cerr read error! std::endl;break;}}}~TcpClient(){if (_sock 0){close(_sock);}} private:int _sock; //文件描述符std::string _server_ip; //服务端的IP地址int _server_port; //服务端的端口号//客户端必须要知道它要连接的服务端的IP地址和端口号//才能通过connect()与特定的服务端进行通信 };tcp_client.cc void Usage(std::string proc) {std::cout Usage: proc server_ip server_port std::endl; } int main(int argc, char* argv[]) {if (argc ! 3){Usage(argv[0]);exit(1);}std::string server_ip argv[1];int server_port atoi(argv[2]);TcpClient* clt new TcpClient(server_ip, server_port);clt-InitClient();clt-Start();return 0; }服务端与客户端的运行效果         先启动服务端再启动客户端。客户端能够向服务端发送数据和接收服务端的响应数据服务端能够接收客户端发送的数据和向客户端响应数据。         客户端退出时服务端停止为其提供服务但不会退出。 3单执行流服务端并不实用 以上服务端被实现为单执行流的。 仅用一个客户端连接一个单执行流服务端时这一个客户端能够正常享受到服务端的服务。但有多个客户端要一个连接单执行流服务端时单执行流服务端就无法正常为它们提供服务了。 一个客户端先连接上单执行流服务端服务端能正常为其提供服务。         但另一个客户端也上单执行流服务端这个客户端发送给服务端的消息既没有在服务端进行打印服务端也没有将该数据回显给该客户端。         只有当第一个客户端退出后服务端才会将第二个客户端发来是数据进行打印并回显该第二个客户端。         服务端调用 accept() 获取数据请求后为发送请求的客户端提供服务但由于当前服务端是单执行流的只能服务完当前客户端后才能继续服务下一个客户端。         在服务端为第一个客户端提供服务期间第二个客户端向服务端发起的数据请求时是成功的但服务端并没有调用 accept() 获取它的数据请求于是服务端就没有显示连接成功。         在底层系统维护了一个连接队列服务端没有获取的新数据请求会被放到这个连接队列中因此虽然服务端没有获取第二个客户端发来的请求但第二个客户端会显示连接成功。 单执行流服务端一段时间内只能为一个客户端提供服务这样服务端的资源既没有充分利用也不符合实际的可能有多个客户端要连接服务端的情景因此服务端一般不会被实现为单执行流的而要实现为多执行流的。 4多进程版本的服务端 服务端为客户端提供服务的过程可以由父子进程来共同完成可以由父进程负责获取客户端的数据请求获取到后可以创建子进程由子进程进一步去为客户端提供服务。 创建的子进程会继承父进程的文件描述符表。 如果父进程已经打开了一个文件其文件描述符是 3此时父进程创建的子进程的 3 号文件描述符也会指向这个已经打开的文件而如果子进程继续创建了一个子进程也就是孙子进程那么孙子进程的 3 号文件描述符同样也会指向这个已经打开的文件。但父子进程之间始终会保持独立性父进程的文件描述符表所发生的变化并不会影响到子进程继承的文件描述符表。 一般来说父进程需要等待子进程退出以免子进程变成僵尸进程。 一般父进程创建出子进程后还需调用 wait() 或 waitpid() 等待子进程退出。 等待的方式一般有两种阻塞式等待和非阻塞轮询。 如果服务端采用阻塞式等待那就与单执行流版本一样仍然要等待服务完当前客户端才能继续获取下一个数据请求。 如果服务端采用非阻塞轮询虽然在子进程为客户端提供服务期间服务端可以继续获取新的数据请求但还需要将所有子进程的PID保存下来同时不停地检测子进程的退出状况。 总得来说父进程要等待子进程退出无论采用什么等待方式服务端的功能都显得不尽人意还不如让父进程别等待子进程退出了。 父进程不等待子进程退出 让父进程不等待子进程退出也有两种常见方式 一种则是捕捉子进程的 SIGCHLD 信号将信号的处理动作设置为忽略。 另一种则是在父进程创建子进程后子进程再创建孙子进程让孙子进程去为客户端提供服务。 .1- 捕捉信号版 子进程退出时会给父进程发送 SIGCHLD 信号如果父进程将 SIGCHLD 信号进行捕捉并将信号的处理动作设置为忽略就可以专心处理自己的工作而无需关心子进程的退出情况了。 tcp_server.hpp ​ #include iostream #include cstdio #include string #include cerrno #include cstring #include cstdlib #include strings.h #include sys/socket.h #includesys/types.h #includesys/stat.h #includefcntl.h #includeunistd.h #include netinet/in.h #include arpa/inet.h #include signal.h #define BACKLOG 5class TcpServer { public:TcpServer(int port): _listen_sock(-1), _port(port){}void InitServer(){//1.创建套接字_listen_sock socket(AF_INET, SOCK_STREAM, 0);if (_listen_sock 0){std::cerr socket error std::endl;exit(2);}//2.绑定struct sockaddr_in local;memset(local, \0, sizeof(local));local.sin_family AF_INET;local.sin_port htons(_port);local.sin_addr.s_addr INADDR_ANY;if (bind(_listen_sock, (struct sockaddr*)local, sizeof(local)) 0){std::cerr bind error std::endl;exit(3);}//3.监听if (listen(_listen_sock, BACKLOG) 0){std::cerr listen error std::endl;exit(4);}}//4.运行void Service();void Start(){//忽略SIGCHLD信号signal(SIGCHLD, SIG_IGN); while(1){//父进程负责不断获取客户端的数据请求struct sockaddr_in peer;memset(peer, \0, sizeof(peer));socklen_t len sizeof(peer);int sock accept(_listen_sock, (struct sockaddr*)peer, len);//父进程获取失败则跳过本次继续获取if (sock 0){std::cerr accept error, continue next std::endl;continue;}std::string client_ip inet_ntoa(peer.sin_addr);int client_port ntohs(peer.sin_port);std::cout get a new link- sock [ client_ip ]: client_port std::endl;//父进程获取成功则创建子进程去提供服务pid_t id fork();if (id 0){ //childService(sock, client_ip, client_port);//子进程提供完服务就退出exit(0); }}}void Service(int sock, std::string client_ip, int client_port){char buffer[1024];while (1){ssize_t size read(sock, buffer, sizeof(buffer)-1);if (size 0){ buffer[size] \0;std::cout client_ip : client_port # buffer std::endl;write(sock, buffer, size);} else if (size 0){ std::cout client_ip : client_port close! std::endl;break;}else{ std::cerr sock read error! std::endl;break;}}close(sock); std::cout client_ip : client_port service done! std::endl;}private:int _listen_sock; //待监听的套接字int _port; //端口号 };​ 【补】进程监控脚本 while :; do ps axj | head -1 ps axj | grep tcp_server | grep -v grep;echo ######################;sleep 1;done服务端与客户端运行效果 ①客户端 1 运行并向服务端发送消息。 ②客户端 2 运行并向服务端发送消息。 ③客户端 2 退出。 ④客户端 1 退出。 .2- 爷孙进程版 让爷爷进程创建的爸爸进程再创建出孙子进程可以让孙子进程去为客户端提供服务 也不用等待孙子进程退出。 具体的实现方式是让爷爷进程在创建爸爸进程后等待爸爸进程退出并让爸爸进程在创建完孙子进程后立刻退出这样一来爷爷进程就能立刻等待成功继续获取其他客户端的数据请求。孙子进程会因爸爸进程的立即退出而变成孤儿进程进而会被系统领养之后就由系统来回收孙子进程的退出信息爷爷进程也无需关心孙子进程的退出情况。 但要注意的是爸爸进程会继承爷爷进程的监听套接字但爸爸进程继承的监听套接字不会被用到因此最好将其关闭以免后续对监听套接字中的数据造成影响。同理孙子进程为客户端提供服务会用到爷爷进程获取到的套接字即 accept() 的返回值因此在服务结束后需关闭这个套接字以免浪费资源和影响之后系统分配文件描述符。 【Tips】爷孙进程版的实现要点 爷爷进程负责调用accept()获取客户端数据请求、创建爸爸进程、关闭accept() 返回的套接字。爸爸进程负责创建孙子进程、关闭继承的监听套接字。孙子进程负责为客户端提供服务。 tcp_server.hpp ​ #include iostream #include cstdio #include string #include cerrno #include cstring #include cstdlib #include strings.h #include sys/socket.h #includesys/types.h #includesys/stat.h #includefcntl.h #includeunistd.h #include netinet/in.h #include arpa/inet.h #includesys/types.h #includesys/wait.h#define BACKLOG 5class TcpServer { public:TcpServer(int port): _listen_sock(-1), _port(port){}void InitServer(){//1.创建套接字_listen_sock socket(AF_INET, SOCK_STREAM, 0);if (_listen_sock 0){std::cerr socket error std::endl;exit(2);}//2.绑定struct sockaddr_in local;memset(local, \0, sizeof(local));local.sin_family AF_INET;local.sin_port htons(_port);local.sin_addr.s_addr INADDR_ANY;if (bind(_listen_sock, (struct sockaddr*)local, sizeof(local)) 0){std::cerr bind error std::endl;exit(3);}//3.监听if (listen(_listen_sock, BACKLOG) 0){std::cerr listen error std::endl;exit(4);}}//4.运行void Service();void Start(){while(1){//爷爷进程负责不断获取客户端的数据请求struct sockaddr_in peer;memset(peer, \0, sizeof(peer));socklen_t len sizeof(peer);int sock accept(_listen_sock, (struct sockaddr*)peer, len);if (sock 0){std::cerr accept error, continue next std::endl;continue;}std::string client_ip inet_ntoa(peer.sin_addr);int client_port ntohs(peer.sin_port);std::cout get a new link- sock [ client_ip ]: client_port std::endl;//创建爸爸进程pid_t id fork();if (id 0){ //需关闭爸爸进程继承的监听套接字close(_listen_sock); //爸爸进程负责创建孙子进程并直接退出if (fork() 0){exit(0); }//孙子进程用accept()返回的套接字为客户端提供服务并在提供完毕后退出Service(sock, client_ip, client_port); exit(0); }//爷爷进程关闭accept()返回的套接字close(sock); //爷爷进程等待爸爸进程退出会立刻等待成功waitpid(id, nullptr, 0); }}void Service(int sock, std::string client_ip, int client_port){char buffer[1024];while (1){ssize_t size read(sock, buffer, sizeof(buffer)-1);if (size 0){ buffer[size] \0;std::cout client_ip : client_port # buffer std::endl;write(sock, buffer, size);} else if (size 0){ std::cout client_ip : client_port close! std::endl;break;}else{ std::cerr sock read error! std::endl;break;}}close(sock); std::cout client_ip : client_port service done! std::endl;}private:int _listen_sock; //待监听的套接字int _port; //端口号 };​ 服务端和客户端的运行效果 ①客户端 1 运行并向服务端发送消息psPID为1的即是孤儿进程。 ②客户端 2 运行并向服务端发送消息。 ③客户端 2 退出。 ④客户端 1 退出。 5多线程版本的服务端 进程的创建伴随着进程控制块、进程地址空间、页表等数据结构的创建而线程本质是在一个进程进程地址空间内运行线程的创建会共享一个进程的大部分资源因此创建线程的成本比创建进程的成本要小得多。 服务端为客户端提供服务的过程也可以由线程来完成。服务端的代码在运行后会生成一个进程作为主要的执行流负责不断获取客户端的数据请求而这个主执行流也就是主线程之后可以创建子线程让子线程去负责为客户端提供服务。 类似的主线程在创建出子线程后也需要等待子线程退出以免造成像僵尸进程一样的内存泄漏问题。当然也可以进行线程分离让系统自动回收子线程所的资源从而让主线程不必关心子线程的退出情况可以专心做自己的事。 属于同一个进程的所有线程会共享同一张文件描述符表 文件描述符表维护的是进程与文件之间的对应关系一个进程对应了一张文件描述符表。而主线程创建的子线程属于同一个进程因此并不会为任何子线程创建独立的文件描述符表属于同一个进程的所有线程会共享同一张文件描述符表。 需注意虽然主线程调用 accept() 获取到的套接字其他子线程也能直接访问但子线程并不知道它所服务的客户端对应的是哪一个套接字因此主线程在创建在子线程后还需要告诉子线程它要访问的文件描述符的值。  套接字如何关闭 accept() 返回的套接字不能由主线程来关闭而要在子线程为客户端提供完服务之后由子线程来关闭。 对于监听套接字子线程无需关心也就无需关闭它否则主线程就无法从监听套接字中获取新的数据请求了。 Service() 与 pthread_create() 要匹配 Service() 是上文中负责为客户端提供服务的接口pthread_create() 则负责创建子线程。 #include pthread.h int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg); 功能在进程的主线程中创建一个新的子线程 参数1.thread输出型参数是一个 pthread_t 类型变量的地址用于获取创建成功的线程的TID。2.attr用于设置线程的属性不关心设为 NULL默认属性即可。3.stat_routine一个返回类型为void* 、参数为 void*的函数指针即线程启动后要执行的函数线程例程。4.arg,是传给 start_routine 线程例程的参数不关心设置为 NULL默认方法即可。 返回值创建成功则返回0失败则返回错误码 也就是说 Service() 其实就是子线程的例程要作为第三个参数传给 pthread_create()。 线程例程的参数是由 pthread_create() 负责的但它只有一个且为 void* 类型而上文中 Service() 所需的参数有三个即客户端对应的套接字、IP 地址、端口号因此就需要将它们匹配起来。 具体的实现是Service() 所需的三个参数封装在一个 Param 结构体中主线程在创建子线程时可以定义一个 Param 对象将客户端的套接字、IP 地址、端口号设置在这个 Param 对象当中然后将这个 Param 对象的地址从 Param* 类型强制为void* 类型并传参到线程例程中最终在线程例程中调用 Service()。 class Param { public:Param(int sock, std::string ip, int port): _sock(sock), _ip(ip), _port(port){}~Param(){} public:int _sock;std::string _ip;int _port; };但要注意的是pthread_create() 所需的线程例程是一个参数类型为 void* 、返回类型为 void* 的函数如果想将其定义在 TcpServer 类中就得将其定义为静态成员函数否则这个线程例程的第一个参数是隐藏的 this 指针与 pthread_create() 的参数要求不匹配另外还需要将 Service() 其定义为静态成员函数否则身为静态成员函数的线程例程无法调用还是非静态成员函数的 Service()。 tcp_server.hpp ​ #include iostream #include cstdio #include string #include cerrno #include cstring #include cstdlib #include strings.h #include sys/socket.h #includesys/types.h #includesys/stat.h #includefcntl.h #includeunistd.h #include netinet/in.h #include arpa/inet.h #includesys/types.h #include pthread.h#define BACKLOG 5//封装Service()参数的结构体 class Param { public:Param(int sock, std::string ip, int port): _sock(sock), _ip(ip), _port(port){}~Param(){} public:int _sock;std::string _ip;int _port; };//服务端 class TcpServer { public:TcpServer(int port): _listen_sock(-1), _port(port){}void InitServer(){//1.创建套接字_listen_sock socket(AF_INET, SOCK_STREAM, 0);if (_listen_sock 0){std::cerr socket error std::endl;exit(2);}//2.绑定struct sockaddr_in local;memset(local, \0, sizeof(local));local.sin_family AF_INET;local.sin_port htons(_port);local.sin_addr.s_addr INADDR_ANY;if (bind(_listen_sock, (struct sockaddr*)local, sizeof(local)) 0){std::cerr bind error std::endl;exit(3);}//3.监听if (listen(_listen_sock, BACKLOG) 0){std::cerr listen error std::endl;exit(4);}}//4.运行static void Service(int sock, std::string client_ip, int client_port);static void* HandlerRequest(void* arg);void Start(){while(1){//主线程负责获取客户端的数据请求struct sockaddr_in peer;memset(peer, \0, sizeof(peer));socklen_t len sizeof(peer);int sock accept(_listen_sock, (struct sockaddr*)peer, len);if (sock 0){std::cerr accept error, continue next std::endl;continue;}std::string client_ip inet_ntoa(peer.sin_addr);int client_port ntohs(peer.sin_port);std::cout get a new link- sock [ client_ip ]: client_port std::endl;//创建子线程让子线程去为客户端提供服务Param* p new Param(sock, client_ip, client_port); //定义封装了Service()参数的结构体对象pthread_t tid; //定义子线程的TIDpthread_create(tid, nullptr, HandlerRequest, p); //创建子线程} }//线程例程static void* HandlerRequest(void* arg){//分离子线程让系统负责回收子线程资源pthread_detach(pthread_self()); //获取Service()的参数结构体Param* p (Param*)arg;//在线程例程中调用Service()为客户端提供服务Service(p-_sock, p-_ip, p-_port); //最终释放参数占用的堆空间delete p; return nullptr;}//负责提供服务的接口static void Service(int sock, std::string client_ip, int client_port){char buffer[1024];while (true){ssize_t size read(sock, buffer, sizeof(buffer)-1);if (size 0){ buffer[size] \0;std::cout client_ip : client_port # buffer std::endl;write(sock, buffer, size);}else if (size 0){ std::cout client_ip : client_port close! std::endl;break;}else{ std::cerr sock read error! std::endl;break;}}close(sock); std::cout client_ip : client_port service done! std::endl;}private:int _listen_sock; //待监听的套接字int _port; //端口号 };​ Makefile .PHONY:all all: tcp_client tcp_server tcp_client:tcp_client.ccg -o $ $^ -stdc11tcp_server:tcp_server.ccg -o $ $^ -stdc11 -lpthread.PHONY:clean clean:rm -f tcp_client tcp_server 【补】线程监控脚本 while :; do ps -aL|head -1ps -aL|grep tcp_server;echo ####################;sleep 1;done服务端和客户端的运行效果 ①客户端 1 运行并向服务端发送消息。 ②客户端 2 运行并向服务端发送消息。 ③客户端 2 退出。 ④客户端 1 退出。 6线程池版本的服务端 .1- 多线程版本服务端的弊病 每当有新的数据请求到来时多线程版本服务端的主线程都会重新为发送请求的客户端创建子线程而服务结束后又会将子线程销毁。如果有大量的数据请求服务端要为每一个发送请求的客户端创建一个子线程计算机中的线程越多CPU 的压力就越大调度线程的成本也会很高此时每一个线程再次被调度的周期会相应变长客户端也可能会迟迟得不到应答。 总得来说多线程版本服务端提供服务的过程不仅麻烦还效率低下。 想要继续在此基础上继续优化服务端可以在服务端中引入线程池。 大致的实现是先在服务端内预先创建一批线程创建的数量不必太多CPU 的压力也不会太大只要服务端获取到数据请求就让这些线程为客户端提供服务使客户端一来就有线程为其提供服务无需等到客户端来了才创建线程。预先创建的线程为客户端提供完服务后也不要让其退出而让其继续为下一个客户端提供服务。 如果当前没有数据请求还可以让线程们进入休眠状态等到有数据请求到来时再将它们唤醒。 如果有数据请求到来但预先创建的线程都在给其他客户端提供服务此时也不必再创建线程而让这个新到来的请求在全连接队列中排队等有空闲的线程资源后再为其提供服务。 .2- 线程池的引入 在服务端提供服务的过程中服务端每接收一个客户端发来的数据请求就要为相应的客户端提供服务而为相应的客户端提供服务这个过程可以看作是一个任务。 现设计一个 Task 类其中封装了客户端对应的套接字、IP地址、端口号以表明一个任务对象是为哪一个客户端提供服务、相应的套接字是哪一个。 此外Task 类中还需要有任务的处理方法作为类成员以便为客户端提供服务。 此处再设计一个 Handler 类在 Handler 类中包含了 ( ) 操作符的重载函数其函数体被定义为与上文中Service() 相同的内容这样就能以仿函数的方式被封装进 Task 类中。 ThreadPool.hpp class Handler //Service() { public:Handler(){}~Handler(){}void operator()(int sock, std::string client_ip, int client_port){char buffer[1024];while (1){ssize_t size read(sock, buffer, sizeof(buffer)-1);if (size 0){ buffer[size] \0;std::cout client_ip : client_port # buffer std::endl;write(sock, buffer, size);}else if (size 0){ std::cout client_ip : client_port close! std::endl;break;}else{ std::cerr sock read error! std::endl;break;}}close(sock); std::cout client_ip : client_port service done! std::endl;} };class Task //任务 { public://无参构造Task(){}//带参构造Task(int sock, std::string client_ip, int client_port): _sock(sock), _client_ip(client_ip), _client_port(client_port){}//析构~Task(){}//为客户端提供服务的接口void Run(){_handler(_sock, _client_ip, _client_port); //实例化 Handler 对象} private:int _sock; //套接字std::string _client_ip; //IP地址int _client_port; //端口号Handler _handler; //处理方法实际是一个仿函数 };接下来实现线程池。 服务端收到的数据请求后会构造任务对象并将任务对象加入到被封装在线程池中的任务队列以待处理。 当任务队列中有任务时线程池中的线程会先定义出一个任务对象用这个任务对象将任务队列的队首元素保存起来这也是 Task 类既提供了带参构造还提供了一个无参构造的原因方便定义无参对象然后对任务队列进行出队操作以此完成从任务队列中获取任务。 ThreadPool.hpp #pragma once #include iostream #include unistd.h #include queue #include pthread.h #include functional#define NUM 5 //默认池中的线程数量为 5//Service() class Handler { public:Handler(){}~Handler(){}void operator()(int sock, std::string client_ip, int client_port){char buffer[1024];while (1){ssize_t size read(sock, buffer, sizeof(buffer)-1);if (size 0){ buffer[size] \0;std::cout client_ip : client_port # buffer std::endl;write(sock, buffer, size);}else if (size 0){ std::cout client_ip : client_port close! std::endl;break;}else{ std::cerr sock read error! std::endl;break;}}close(sock); std::cout client_ip : client_port service done! std::endl;} };//任务 class Task { public:Task(){}Task(int sock, std::string client_ip, int client_port): _sock(sock), _client_ip(client_ip), _client_port(client_port){}~Task(){}void Run(){_handler(_sock, _client_ip, _client_port); //实例化 Handler 对象} private:int _sock; //套接字std::string _client_ip; //IP地址int _client_port; //端口号Handler _handler; //处理方法实际是一个仿函数 };//线程池 templateclass T class ThreadPool { private://对任务队列验空bool IsEmpty(){return _task_queue.size() 0;}//加锁void LockQueue(){pthread_mutex_lock(_mutex);}//解锁void UnLockQueue(){pthread_mutex_unlock(_mutex);}//挂起休眠void Wait(){pthread_cond_wait(_cond, _mutex);}//唤醒void WakeUp(){pthread_cond_signal(_cond);} public://构造ThreadPool(int num NUM): _thread_num(num){pthread_mutex_init(_mutex, nullptr);pthread_cond_init(_cond, nullptr);}//析构~ThreadPool(){pthread_mutex_destroy(_mutex);pthread_cond_destroy(_cond);}//线程例程static void* Routine(void* arg){ //线程分离pthread_detach(pthread_self());//不断从任务队列获取任务进行处理ThreadPool* self (ThreadPool*)arg;while (1){self-LockQueue(); //加锁while (self-IsEmpty()){self-Wait(); //任务队列为空就让线程休眠等有任务再被唤醒}T task; //定义任务对象self-Pop(task); //任务出队self-UnLockQueue();//解锁task.Run(); //处理任务即提供服务}}//初始化线程池创建线程void ThreadPoolInit(){pthread_t tid;for (int i 0; i _thread_num; i){pthread_create(tid, nullptr, Routine, this); //向线程例程传入this指针传入的其实是线程自己}}//将任务入队void Push(const T task){LockQueue(); //加锁_task_queue.push(task); //任务入队UnLockQueue(); //解锁WakeUp(); //唤醒线程}//获取任务出队void Pop(T task){task _task_queue.front();_task_queue.pop();} private:std::queueT _task_queue; //任务队列int _thread_num; //池中线程的数量pthread_mutex_t _mutex; //互斥锁pthread_cond_t _cond; //条件变量 };引入线程池和任务类后还需在服务端的 TcpServer 类中新增一个线程池成员。 这其实构建了一个生产者消费者模型其中服务端的进程是任务的生产者会不断通过 TcpServer 类中的线程池成员创建任务并将任务加入到任务队列后端线程池中的若干线程是任务的消费者会不断从任务队列中获取任务并进行处理而生产者和消费者的交易场所就是线程池中的任务队列。 tcp_server.hpp #include iostream #include unordered_map #include cstdio #include string #include cerrno #include cstring #include cstdlib #include strings.h #include sys/socket.h #includesys/types.h #includesys/stat.h #includefcntl.h #includeunistd.h #include netinet/in.h #include arpa/inet.h #includesys/types.h #includesys/wait.h #includeThreadPool.hpp#define BACKLOG 5class TcpServer { public:TcpServer(int port): _listen_sock(-1), _port(port), _tp(nullptr)//线程池指针初始化为空等到服务端正式运行后再构造线程池对象{}void InitServer(){//1.创建套接字_listen_sock socket(AF_INET, SOCK_STREAM, 0);if (_listen_sock 0){std::cerr socket error std::endl;exit(2);}//2.绑定struct sockaddr_in local;memset(local, \0, sizeof(local));local.sin_family AF_INET;local.sin_port htons(_port);local.sin_addr.s_addr INADDR_ANY;if (bind(_listen_sock, (struct sockaddr*)local, sizeof(local)) 0){std::cerr bind error std::endl;exit(3);}//3.监听if (listen(_listen_sock, BACKLOG) 0){std::cerr listen error std::endl;exit(4);}_tp new ThreadPoolTask(); //构造线程池对象}//4.运行void Start(){ //初始化线程池(创建线程_tp-ThreadPoolInit(); while(1){//(1)获取数据请求struct sockaddr_in peer;memset(peer, \0, sizeof(peer));socklen_t len sizeof(peer);int sock accept(_listen_sock, (struct sockaddr*)peer, len);if (sock 0){std::cerr accept error, continue next std::endl;continue;}std::string client_ip inet_ntoa(peer.sin_addr);int client_port ntohs(peer.sin_port);std::cout get a new link- sock [ client_ip ]: client_port std::endl;//2处理数据请求Task task(sock, client_ip, client_port); //构造任务_tp-Push(task); //将任务入队进任务队列}}private:int _listen_sock; //监听套接字int _port; //端口号ThreadPoolTask* _tp; //指向线程池的指针 };.3- 测试效果 服务端和客户端的运行效果 ①服务端运行后。 ②客户端 1 运行并向服务端发送消息。 ③客户端 2 运行并向服务端发送消息。 ④客户端 2 退出。 ⑤客户端 1 退出。
http://www.hkea.cn/news/14277457/

相关文章:

  • 苏中建设集团网站官网深圳网站建设选哪家
  • 用软件做模板下载网站大型网站 cms
  • 邢台县教育局五库建设网站高效网站推广设计
  • 怎么申请建立个人免费网站天津哪里有做网站的
  • 请人做网站注意事项网站标题的关键字怎么写
  • 电力建设工程质量监督总网站周口高端网站建设
  • 桐城做网站的公司余干县建设局网站
  • 做美食软件视频网站电商网站建设运营协议
  • 国外购物网站怎么做网站代运营服务内容有
  • wordpress跳转到子页面郑州网络营销网站优化
  • 旅游网站开发的流程网站推广途径及要点
  • 织梦网站上传数据库微信 wordpress搜索
  • 自己做下载类网站免费申请商家收款码
  • 行业网站盈利模式国外网站建设的发展
  • 网站建设与管理 pdfwordpress 需要ftp
  • 电商首页模板网站阿凡达网站建设网
  • 建站网站知乎服装图案素材网站
  • php网站的数据库在哪网站建设实训意义
  • 网站建站和项目部署一样吗做公司网站要那些资料
  • 西安演出公司网站建设集团网站 wordpress
  • 网站改版方案ppt订阅号可以做微网站
  • 广西建设协会网站黑龙江建设网站
  • 格力电器的网站建设评价wordpress按最后评论排序
  • 网站建设推广视频深圳做网站可用乐云seo十年
  • 漳州最具口碑的网站建设wordpress wpenqueuescript
  • 深圳网站建设 易通鼎网站建设与网页设计开题报告
  • 网页 网站网站开发可以用哪些语言
  • 什么网站做电脑系统好校园兼职网站建设
  • 基础网站建设素材国内开源建站cms
  • 徐州住房与城乡建设部网站青岛谁优化网站做的好处