欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 房产 > 建筑 > 网络编程套接字

网络编程套接字

2025/3/31 17:08:03 来源:https://blog.csdn.net/weixin_72501602/article/details/146529884  浏览:    关键词:网络编程套接字

目录

一、概念理解        

1.1 源IP地址和目的IP地址        

1.2 源端口号和目的端口号       

1.3 TCP协议

1.4 UDP协议

1.5 网络字节序

二、socket

2.1 socket 常见API

2.2 sockaddr 结构体        

2.3 地址转换函数        

2.4 TCP socket API  和 UDP socket API 

三、简单的UDP网络程序        

3.1 封装 UdpSocket       

3.2 UDP通用服务器       

3.3 英译汉服务器        

3.4 UDP通用客户端       

3.5 英译汉客户端        

四、简单的TCP网络程序         

4.1 TCP socket API 详解         

4.1.1 socket()      

4.1.2 bind()            

4.1.3 listen()

4.1.4 accept()       

4.1.5 connect()        

4.2 封装 TCP socket         

4.3 TCP通用服务器       

4.4 英译汉服务器       

4.5 TCP通用客户端

4.6 英译汉客户端       

五、其他补充        

5.1 多进程版本的TCP程序 

5.2 多线程版本的TCP程序

5.3 线程池版本的 TCP 服务器

5.4 了解TCP协议通讯流程        



一、概念理解        

1.1 源IP地址和目的IP地址        

        在IP数据包头部中,有两个IP地址,分别叫做源IP地址目的IP地址。举个例子,唐僧前往西天取经,但他不认识路,因此每到一个地方就会如此问路:“贫僧从东土大唐而来,前往西天取经”,然后路人便会告知这里是哪里,下一站在哪里,该怎么走。
        在网络通信中,源IP地址目的IP地址帮助数据包在网络中找到正确的来源和目的地。网络中的路由器和交换机使用目的IP地址来确定如何将数据包从源设备传输到目标设备。在这一过程中,数据包会经过多个中间设备(路由器),它们根据目的IP地址决定下一步的转发路径。源IP地址和目的IP地址共同作用,确保数据能够准确地从发送者到达接收者。例如,源IP地址告诉接收方数据是从哪里来的,而目的IP地址则让接收方知道这个数据包是专门发给它的。        

        

1.2 源端口号和目的端口号       

        我们光有IP地址就可以完成通信了嘛? 想象一下你在qq上给你的朋友发了一条消息, 有了IP地址能够把消息发送到对方的机器上,但是你的朋友最终看到的并不是数据包。实际上还需要有一个额外的标识来区分出,这个数据要给哪个程序进行解析。这个标识就是端口号。        

        端口号(port)是传输层协议的内容。端口号是一个2字节16位的整数;端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理;IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;一个端口号只能被一个进程占用,但是一个进程可以绑定多个端口号。

        在传输层协议(如TCP和UDP)中,每个数据段都包含两个端口号,分别是源端口号指明数据的发送者,即数据包是由哪个程序或进程发出的】和目的端口号指明数据的接收者,即数据包应当交给目标计算机上的哪个程序或服务来处理】。这两个端口号就像是在描述“数据是谁发的, 要发给谁”。想象一下你在寄送一份快递。快递包裹上有两个重要的信息:寄件人地址(源端口号):这就相当于“源端口号”,它告诉收件人这份快递是从哪个地方发出的。这个地址用于返回信息,确保回应能够找到正确的寄件人。收件人地址(目的端口号):这就相当于“目的端口号”,它告诉快递员,包裹应该送到谁那里。这个地址指向接收者的“服务”或“程序”,确保数据送到正确的地方(比如浏览器、聊天程序等)。

1.3 TCP协议

        TCP(Transmission Control Protocol,传输控制协议)是一种非常重要且基础的协议,它提供了连接可靠字节流传输功能。其核心优势在于确保数据能够完整、顺序地到达目的地,并且在传输过程中处理可能出现的错误。

TCP的特点

  • 面向连接(Connection-Oriented):
    • 在传输数据之前,TCP需要在通信的两端建立一个连接。这就像是打电话之前,你和对方需要先通话建立联系一样。
    • 这个过程称为三次握手(Three-way Handshake),即客户端和服务器相互确认,确保它们准备好进行数据传输。
  • 可靠传输(Reliable Transmission):
    • TCP确保数据的可靠性。传输过程中,如果数据丢失或损坏,接收方会通知发送方重新发送丢失或损坏的数据。
    • 每个数据包都有序列号,接收方会按顺序确认收到的数据包。这样可以确保接收到的数据完整且无误。
  • 面向字节流(Byte-Stream Oriented):
    • TCP传输的数据是连续的字节流,没有明确的消息边界。发送方将应用层的数据转换成一串字节流,接收方再将字节流还原为应用层的数据。
    • 不管你发送的是什么类型的数据(例如文本、视频、音频等),TCP都将它们视作字节流来处理。因此,应用层并不需要关心数据是否完整或按顺序到达,它只需要通过TCP获取流式的数据即可。

        

1.4 UDP协议

        UDP(User Datagram Protocol,用户数据报协议)是一种简单且快速的传输协议,适用于对速度要求高且可以容忍丢包的应用。它不提供可靠性、顺序性保证,因此使用UDP时需要根据应用的需求自行处理丢包、乱序等问题。

UDP的特点

  • 无连接(Connectionless)

    • UDP是无连接的,意味着发送数据之前不需要建立连接,也不需要在数据传输完毕后关闭连接。每个数据报(数据包)都是独立的,它们相互之间没有关联。
    • 发送方只需要将数据发送出去,接收方收到就行,没有建立、维持连接的过程。
  • 不可靠传输(Unreliable Transmission)

    • UDP不保证数据的可靠性。它不会像TCP一样做数据包的确认、重传等操作。如果在传输过程中数据包丢失、乱序或损坏,UDP不会进行任何补救措施。
    • 这使得UDP的传输速度较快,但也意味着应用层需要自己处理丢包、数据错误等问题(如果需要的话)。
  • 面向数据报(Datagram-Oriented)

    • UDP是面向数据报的,每个数据包被称为数据报。数据报之间是独立的,它们不会合并成一个大的数据流进行传输,每个数据报都包含了完整的目标信息。
    • 每个UDP数据报都有头部和数据部分,其中头部包含了源端口、目标端口、长度、校验和等信息,数据部分就是实际传输的内容。

1.5 网络字节序

        我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分;磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分;网络数据流同样有大端小端之分。那么如何定义网络数据流的地址呢?
        发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址
        TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据。如果当前发送主机是小端机,就需要先将数据转成大端;否则就忽略,直接发送即可。
        网络程序具有可移植性,为使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换

#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uintl6_t htons(uintl6_t hostshort);
uint32_t ntohl(uint32_t netlong);
uintl6_t ntohs(uintl6_t netshort);如果主机是小端字节序,这些函数会将参数做相应的大小端转换然后返回
如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回函数名助记:h表示host,n表示network,l表示32位长整数,s表示16位短整数
例如htonl表示将32位的长整数从主机字节序转换为网络字节序将IP地址转换后准备发送#include <stdio.h>
#include <arpa/inet.h>  // 引入htonl所在的头文件int main() {unsigned long host_ip = 0xC0A80101;  // IP地址 192.168.1.1 的 32 位整数表示unsigned long network_ip = htonl(host_ip);  // 将主机字节序转换为网络字节序printf("Host byte order: 0x%lx\n", host_ip);printf("Network byte order: 0x%lx\n", network_ip);return 0;
}

        


二、socket

2.1 socket 常见API

// 创建 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);

        

2.2 sockaddr 结构体        

        在使用 socket API 进行网络编程时,sockaddr 结构体是一个非常重要的概念,它作为不同协议族(如IPv4、IPv6、UNIX Domain Socket)地址的统一表示形式。       

struct sockaddr {sa_family_t sa_family;  // 地址族 (Address Family)char        sa_data[14];  // 地址数据,长度为14个字节
};

        IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型,16位端口号和32位IP地址。IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6。这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。
        socket API可以都用struct sockaddr *类型表示,在使用的时候需要强制转化成sockaddr_in;这样的好处是可以提高程序的通用性,可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数。
        不同的网络协议对地址有不同的需求,因此会有不同的地址结构体继承 sockaddr 结构体,并根据协议族的要求扩展它,对于 IPv4 地址,使用 sockaddr_in 结构,它扩展了 sockaddr 结构,并为 IPv4 地址提供了具体的字段

struct sockaddr_in {sa_family_t    sin_family;   // 地址族,通常是 AF_INETin_port_t      sin_port;     // 端口号,16位struct in_addr sin_addr;     // IPv4 地址(32位)
};typedef uint32_t in_addr_t;
struct in_addr {in_addr_t s_addr; // IPv4 地址,通常为一个 32 位整数
};//对于 IPv6 地址,则使用的是 sockaddr_in6 结构
//对于 UNIX 域套接字,使用 sockaddr_un 结构

        

2.3 地址转换函数        

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

字符串转in_addr的函数:#include <arpa/inet.h>
int inet_aton(const char *strptr, struct in_addr *addrptr);
in_addr_E inet_addr(const char *strptr);
int inet_pton(int family, const char *strptr,void *addrptr);in_addr转字符串的函数:char *inet_ntoa(struct in_addr inaddr);
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);注意:其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,
还可以转换IPv6的in6_addr,因此函数接口是void*addrptr

示例代码:

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main() {struct sockaddr_in addr;inet_aton("127.0.0.1", &addr.sin_addr);uint32_t* ptr = (uint32_t*)(&addr.sin_addr);printf("addr: %x\n", *ptr);printf("addr_str: %s\n", inet_ntoa(addr.sin_addr));return 0;
}

        关于 inet_ntoa 函数,它返回一个 char* 类型的指针,该指针指向一个存储 IP 地址字符串的内存区域。这个内存区域并不是由调用者申请的,而是由 inet_ntoa 内部管理的。根据 man 手册的说明,inet_ntoa 将返回的 IP 地址保存在静态存储区中。因此,调用者不需要手动释放内存。

        需要注意的是,由于该函数使用静态存储区来保存结果,每次调用时都会覆盖之前的返回值。也就是说,如果多次调用 inet_ntoa,每次返回的 IP 地址会覆盖前一次调用的结果。因此,如果需要在多次调用中保留多个 IP 地址的结果,调用者应当在每次调用后及时保存返回值,而不能依赖于多次调用的返回结果。


        在APUE中,明确提出inet_ntoa不是线程安全的函数。尽管在 CentOS 7 上的测试中没有遇到明显的问题,不能排除操作系统或库的实现做了某些优化(比如加了互斥锁)来避免线程安全问题。推荐在多线程环境下使用 inet_ntop 函数。与 inet_ntoa 不同,inet_ntop 函数要求调用者提供一个缓冲区来存储结果。

#include <stdio.h>
#include <arpa/inet.h>void* thread_func(void* arg) {struct in_addr addr;inet_aton("192.168.1.1", &addr);char ip_buffer[INET_ADDRSTRLEN];inet_ntop(AF_INET, &addr, ip_buffer, INET_ADDRSTRLEN);printf("IP Address: %s\n", ip_buffer);return NULL;
}int main() {pthread_t threads[5];// 创建多个线程来测试for (int i = 0; i < 5; ++i) {pthread_create(&threads[i], NULL, thread_func, NULL);}// 等待所有线程完成for (int i = 0; i < 5; ++i) {pthread_join(threads[i], NULL);}return 0;
}

2.4 TCP socket API  和 UDP socket API 

特性TCPUDP
连接方式面向连接(需要建立连接)无连接(无需建立连接)
可靠性可靠(确保数据顺序和完整性)不可靠(数据可能丢失、重复或乱序)
数据传输面向字节流,数据流连续面向数据报,每个数据包独立
传输速度较慢(需要连接、确认、重传等机制)较快(无连接、没有重传机制)
常见应用HTTP、FTP、SMTP、Telnet 等视频流、DNS、VoIP、实时游戏等

        TCP 和 UDP 是两种不同的传输协议,它们在使用 Socket API 时有一些区别。主要的区别在于连接的方式、数据传输的可靠性以及如何进行数据传输。        

TCP(传输控制协议):

  • 面向连接:TCP 是一种可靠的、面向连接的协议,在数据传输之前,客户端和服务器之间必须先建立连接(即通过三次握手完成连接)。
  • 在 TCP 中,客户端和服务器之间的数据传输是通过建立的连接进行的,必须先调用 connect()(客户端)和 accept()(服务器)进行连接的建立。
socket():用于创建一个 TCP 套接字,type 参数为 SOCK_STREAM。
bind():绑定地址和端口(通常用于服务器)。
listen():将套接字设置为监听状态,等待客户端连接。
accept():接受客户端连接请求,返回一个新的套接字用于通信。
connect():客户端向服务器发起连接请求。
read()/write() 或 recv()/send():用于数据传输。
close():关闭连接。

UDP(用户数据报协议):

  • 无连接:UDP 是一种无连接的协议,数据传输时无需建立连接。客户端直接向服务器发送数据,服务器直接接收数据。
  • 在 UDP 中,使用 sendto() 和 recvfrom() 进行数据传输,而没有 connect() 和 accept() 步骤。
socket():用于创建一个 UDP 套接字,type 参数为 SOCK_DGRAM。
bind():绑定地址和端口(通常用于服务器)。
sendto():发送数据到指定的目标地址和端口。
recvfrom():从指定的源地址接收数据。
close():关闭套接字。

        


三、简单的UDP网络程序        

        实现一个简单的英译汉的服务功能。        

3.1 封装 UdpSocket       

// udp_socket.hpp#pragma once
#include <stdio.h>       // 标准输入输出库,用于 perror()
#include <string.h>      // 字符串操作函数库,用于处理字符串
#include <stdlib.h>      // 标准库,包含一般性的 C 函数
#include <cassert>       // 用于断言函数的头文件
#include <string>        // C++ 标准库字符串类 std::string
#include <unistd.h>      // UNIX 标准函数库,包含 close() 函数
#include <sys/socket.h>  // 套接字编程相关的函数头文件
#include <netinet/in.h>  // 网络协议族相关定义(如 sockaddr_in)
#include <arpa/inet.h>   // 包含 inet_addr() 和 inet_ntoa() 函数// 定义 sockaddr 和 sockaddr_in 类型的别名
typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;// 定义 UdpSocket 类,用于封装 UDP 套接字的创建、发送、接收等操作
class UdpSocket {
public:// 构造函数,初始化套接字文件描述符为 -1,表示未创建套接字UdpSocket() : fd_(-1) {}// 创建 UDP 套接字bool Socket() {// 创建一个 UDP 套接字fd_ = socket(AF_INET, SOCK_DGRAM, 0);if (fd_ < 0) {  // 如果套接字创建失败,返回 falseperror("socket");  // 输出错误信息return false;}return true;  // 套接字创建成功,返回 true}// 关闭 UDP 套接字bool Close() {close(fd_);  // 关闭套接字文件描述符return true;}// 绑定本地地址和端口bool Bind(const std::string& ip, uint16_t port) {sockaddr_in addr;addr.sin_family = AF_INET;  // 设置地址族为 IPv4addr.sin_addr.s_addr = inet_addr(ip.c_str());  // 转换 IP 地址为网络字节序addr.sin_port = htons(port);  // 设置端口号,并转换为网络字节序// 绑定套接字到指定的 IP 地址和端口int ret = bind(fd_, (sockaddr*)&addr, sizeof(addr));if (ret < 0) {  // 绑定失败,返回 falseperror("bind");  // 输出错误信息return false;}return true;  // 绑定成功,返回 true}// 接收数据bool RecvFrom(std::string* buf, std::string* ip = NULL, uint16_t* port = NULL) {char tmp[1024 * 10] = { 0 };  // 临时缓冲区,用于存放接收到的数据sockaddr_in peer;  // 存放远程端的地址信息socklen_t len = sizeof(peer);  // 地址结构体的大小// 接收数据,存储在 tmp 缓冲区中,并获取对方的地址信息ssize_t read_size = recvfrom(fd_, tmp, sizeof(tmp) - 1, 0, (sockaddr*)&peer, &len);if (read_size < 0) {  // 接收失败,返回 falseperror("recvfrom");  // 输出错误信息return false;}// 将接收到的缓冲区内容存储到传入的字符串对象 buf 中buf->assign(tmp, read_size);// 如果 ip 参数不为空,将远程端的 IP 地址存入 ipif (ip != NULL) {*ip = inet_ntoa(peer.sin_addr);  // 将网络字节序的 IP 地址转换为字符串}// 如果 port 参数不为空,将远程端的端口号存入 portif (port != NULL) {*port = ntohs(peer.sin_port);  // 将网络字节序的端口号转换为主机字节序}return true;  // 数据接收成功,返回 true}// 发送数据到指定的 IP 和端口bool SendTo(const std::string& buf, const std::string& ip, uint16_t port) {sockaddr_in addr;addr.sin_family = AF_INET;  // 设置地址族为 IPv4addr.sin_addr.s_addr = inet_addr(ip.c_str());  // 转换 IP 地址为网络字节序addr.sin_port = htons(port);  // 设置端口号,并转换为网络字节序// 发送数据到指定的 IP 和端口ssize_t write_size = sendto(fd_, buf.data(), buf.size(), 0, (sockaddr*)&addr, sizeof(addr));if (write_size < 0) {  // 发送失败,返回 falseperror("sendto");  // 输出错误信息return false;}return true;  // 数据发送成功,返回 true}private:int fd_;  // 套接字文件描述符,初始值为 -1,表示未创建套接字
};

3.2 UDP通用服务器       

// udp_server.hpp#pragma once
#include "udp_socket.hpp"  // 引入自定义的 UDP 套接字类,用于网络通信// C++11 的方式定义一个 Handler 类型,代表处理请求的函数。
// Handler 是一个函数类型,接受一个 const std::string& 参数(请求),
// 返回 void,并且通过 std::string* 作为响应的输出参数。
// 这种方式可以支持函数指针、仿函数以及 Lambda 表达式。
#include <functional>
typedef std::function<void(const std::string&, std::string* resp)> Handler;// UdpServer 类:封装了 UDP 服务端的主要功能,包括创建套接字、绑定端口、接收请求、处理请求和发送响应。
class UdpServer {
public:// 构造函数:创建 UDP 套接字并进行初始化UdpServer() {assert(sock_.Socket());  // 确保套接字创建成功,如果失败则断言}// 析构函数:关闭套接字,释放资源~UdpServer() {sock_.Close();  // 关闭套接字}// 启动 UDP 服务,参数 ip 和 port 是绑定的地址和端口,handler 是处理请求的函数bool Start(const std::string& ip, uint16_t port, Handler handler) {// 1. 创建 UDP 套接字并绑定端口bool ret = sock_.Bind(ip, port);  // 将套接字绑定到指定的 IP 和端口if (!ret) {  // 如果绑定失败,返回 falsereturn false;}// 2. 进入事件循环,等待并处理客户端请求for (;;) {// 3. 尝试读取客户端请求std::string req;  // 用于存储接收到的请求数据std::string remote_ip;  // 记录发送请求的客户端 IP 地址uint16_t remote_port = 0;  // 记录发送请求的客户端端口号bool ret = sock_.RecvFrom(&req, &remote_ip, &remote_port);  // 接收数据if (!ret) {  // 如果接收失败,继续等待下一个请求continue;}// 4. 创建响应字符串std::string resp;// 5. 根据请求数据计算响应,调用传入的 handler 函数handler(req, &resp);  // 使用 handler 处理请求并生成响应// 6. 将响应数据发送回客户端sock_.SendTo(resp, remote_ip, remote_port);  // 发送响应给请求的客户端// 打印日志,记录请求和响应信息printf("[%s:%d] req: %s, resp: %s\n", remote_ip.c_str(), remote_port,req.c_str(), resp.c_str());}// 关闭套接字,退出事件循环sock_.Close();return true;  // 返回服务启动成功}private:UdpSocket sock_;  // 创建一个 UdpSocket 对象,负责网络通信
};

3.3 英译汉服务器        

// dict_server.cc#include "udp_server.hpp"  // 引入自定义的 UDP 服务器头文件
#include <unordered_map>    // 引入 unordered_map 用于存储字典
#include <iostream>         // 引入输入输出流库// 使用全局字典 g_dict 来存储单词和其对应的翻译
std::unordered_map<std::string, std::string> g_dict;// 定义翻译函数,接收请求字符串并填充响应字符串
void Translate(const std::string& req, std::string* resp) {// 在字典中查找请求的单词auto it = g_dict.find(req);// 如果字典中找不到该单词,返回“未查到!”if (it == g_dict.end()) {*resp = "未查到!";return;}// 找到则返回对应的翻译*resp = it->second;
}int main(int argc, char* argv[]) {// 检查命令行参数,确保传入了正确的 IP 和端口if (argc != 3) {printf("Usage ./dict_server [ip] [port]\n");  // 输出使用说明return 1;}// 1. 数据初始化:将一些常用的单词及其翻译插入字典中g_dict.insert(std::make_pair("hello", "你好"));  g_dict.insert(std::make_pair("world", "世界"));  g_dict.insert(std::make_pair("code", "代码"));  g_dict.insert(std::make_pair("project", "对象"));  // 2. 启动服务器,传入服务器的 IP 和端口,以及翻译函数作为回调UdpServer server;server.Start(argv[1], atoi(argv[2]), Translate);  // 启动 UDP 服务器,监听指定端口并处理请求return 0;  // 程序结束
}

3.4 UDP通用客户端       

// udp_client.hpp#pragma once
#include "udp_socket.hpp"  // 引入自定义的 UDP 套接字头文件// 定义 UdpClient 类,用于封装 UDP 客户端的功能
class UdpClient {
public:// 构造函数,初始化客户端的 IP 地址和端口UdpClient(const std::string& ip, uint16_t port) : ip_(ip), port_(port) {// 创建一个 UDP 套接字,并检查是否成功创建assert(sock_.Socket());}// 析构函数,关闭套接字~UdpClient() {sock_.Close();  // 关闭套接字,释放资源}// 从服务器接收数据bool RecvFrom(std::string* buf) {return sock_.RecvFrom(buf);  // 调用套接字的 RecvFrom 函数接收数据}// 向服务器发送数据bool SendTo(const std::string& buf) {return sock_.SendTo(buf, ip_, port_);  // 调用套接字的 SendTo 函数发送数据到指定的服务器}private:UdpSocket sock_;  // UdpSocket 对象,用于实际的网络通信std::string ip_;  // 服务器的 IP 地址uint16_t port_;   // 服务器的端口号
};

3.5 英译汉客户端        

// dict_client.cc#include "udp_client.hpp"  // 引入自定义的 UDP 客户端类头文件
#include <iostream>         // 引入标准输入输出库int main(int argc, char* argv[]) {// 检查命令行参数数量是否正确if (argc != 3) {printf("Usage ./dict_client [ip] [port]\n");  // 输出用法提示return 1;  // 参数不正确,程序退出}// 创建一个 UdpClient 对象,传入服务器的 IP 和端口UdpClient client(argv[1], atoi(argv[2]));// 进入一个无限循环,等待用户输入要查询的单词for (;;) {std::string word;std::cout << "请输入您要查的单词: ";  // 提示用户输入单词std::cin >> word;  // 从标准输入读取用户输入的单词// 如果输入无效(如用户输入流出现问题),则退出循环if (!std::cin) {std::cout << "Good Bye" << std::endl;  // 输出告别语break;  // 结束循环}// 向服务器发送查询的单词client.SendTo(word);std::string result;// 接收服务器返回的单词含义client.RecvFrom(&result);// 输出查询结果std::cout << word << " 意思是 " << result << std::endl;}return 0;  // 正常结束程序
}

        


四、简单的TCP网络程序         

4.1 TCP socket API 详解         

4.1.1 socket()      

#include <sys/types.h>  // For types used in socket programming
#include <sys/socket.h> // For the socket() function and related constantsint socket(int domain, int type, int protocol);domain(协议族): 对于IPv4协议,您可以使用 AF_INET 来表示IPv4地址族。
type(套接字类型): 通常,您会选择 SOCK_STREAM 作为 TCP 协议的套接字类型,这种类型是面向连接、可靠的流式传输协议,适用于大部分网络通信。
protocol: 一般情况下,您可以将其设置为 0,操作系统会根据 domain 和 type 自动选择适当的协议(例如,选择 IPPROTO_TCP 作为协议)。

        socket() 函数用于打开一个网络通信端口,成功时返回一个类似于 open() 的文件描述符,应用程序可以通过 read()write() 函数像操作文件一样进行网络数据的收发。若 socket() 调用失败,则返回 -1。对于IPv4协议,family 参数应指定为 AF_INET;对于TCP协议,type 参数应设置为 SOCK_STREAM,表示面向连接的流式传输协议。protocol 参数通常设置为 0,由操作系统自动选择合适的协议。     

4.1.2 bind()            

#include <sys/types.h>  
#include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);sockfd:由 socket() 创建的套接字文件描述符。
addr:指向 sockaddr 结构的指针,该结构包含本地地址信息(如 IP 地址和端口号)。
addrlen:addr 结构的长度,通常使用 sizeof(struct sockaddr_in)。

        服务器程序监听的网络地址和端口号通常是固定的,客户端在得知服务器的地址和端口后,可以向服务器发起连接。服务器端需要调用 bind() 函数,将指定的网络地址和端口号绑定到套接字。bind() 成功时返回 0,失败时返回 -1。

        bind() 的作用是将套接字 sockfd 与指定的地址 myaddr 绑定,使得 sockfd 用于监听 myaddr 所描述的地址和端口号。由于 struct sockaddr* 是一个通用指针类型,myaddr 参数实际上可以接受多种协议的 sockaddr 结构体,而这些结构体的长度不同,因此需要通过第三个参数 addrlen 来指定结构体的长度。

        myaddr 参数的初始化过程如下:        

  1. 清零结构体:使用 bzero(&servaddr, sizeof(servaddr)) 将整个 servaddr 结构体清零,确保结构体中的所有字段初始化为零,避免未定义的值影响后续操作。

  2. 设置地址类型servaddr.sin_family = AF_INET 设置地址族为 AF_INET,即IPv4地址族,表示服务器使用的是IPv4协议。

  3. 设置网络地址servaddr.sin_addr.s_addr = htonl(INADDR_ANY) 设置网络地址为 INADDR_ANY,该宏表示绑定到本地的任意IP地址。服务器可能有多个网卡,且每个网卡可能绑定多个IP地址,使用 INADDR_ANY 可以使服务器在所有可用的IP地址上监听,直到与客户端建立连接时,确定实际使用的IP地址。

  4. 设置端口号servaddr.sin_port = htons(SERV_PORT) 设置端口号为 SERV_PORT,在此例中,SERV_PORT 被定义为 9999。htons() 函数用于将端口号从主机字节序转换为网络字节序,确保跨平台的一致性。

4.1.3 listen()

#include <sys/types.h>  
#include <sys/socket.h>int listen(int sockfd, int backlog);sockfd:表示服务器套接字的文件描述符,这个套接字通常是由 socket() 创建的,并且已经通过 bind() 绑定了一个网络地址和端口号。
backlog:指定待连接队列的最大长度,即系统中可以等待连接的客户端的数量。如果客户端在连接时,服务器尚未处理该连接,那么这些连接会被放入待连接队列。该值的大小会影响系统的性能和并发能力。

        listen()sockfd 设置为监听状态,最多允许 backlog 个客户端处于连接等待队列中。如果接收到超过该数量的连接请求,则会被忽略。一般情况下,backlog 设置不会太大,通常为 5,具体细节可课后深入研究。listen() 成功时返回 0,失败时返回 -1。        

4.1.4 accept()       

#include <sys/types.h>  
#include <sys/socket.h>int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);sockfd: 是监听套接字的文件描述符,即通过 socket() 创建并绑定了地址和端口,并通过 listen() 进行监听的套接字。
addr: 是一个指向 sockaddr 结构体的指针,用于保存客户端的地址信息。
addrlen: 是一个指向 socklen_t 类型的指针,用于表示 addr 结构体的大小,函数执行后,addrlen 会被填充为实际的地址长度。while (1) {// 获取客户端地址结构的大小cliaddr_len = sizeof(cliaddr);// 接受客户端连接,返回一个新的套接字连接connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);// 从连接中读取数据,存入buf中n = read(connfd, buf, MAXLINE);// 处理数据...// 关闭与客户端的连接close(connfd);
}

        在三次握手完成后,服务器调用 accept() 接受客户端的连接请求。如果此时没有客户端的连接请求,accept() 会阻塞,直到有客户端连接上来。addr 是一个传出参数,用于返回客户端的地址和端口号。如果不关心客户端的地址,可以将 addr 设置为 NULLaddrlen 是一个传入传出的参数,传入时表示调用者提供的 addr 缓冲区长度,以避免缓冲区溢出;返回时,addrlen 会被更新为客户端地址结构体的实际长度,可能小于调用者提供的缓冲区大小。        

4.1.5 connect()        

#include <sys/types.h>  
#include <sys/socket.h>int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);sockfd:是一个已经创建的套接字描述符,通常是通过 socket() 系统调用创建的。
addr:是指向一个 sockaddr 结构的指针,这个结构包含了目标服务器的地址信息(包括 IP 地址和端口)。
addrlen:是 addr 结构的大小,通常是 sizeof(struct sockaddr_in),即目标服务器地址的长度。

        客户端需要调用 connect() 函数连接服务器。connect()bind() 的参数形式相似,区别在于 bind() 使用的是客户端的本地地址,而 connect() 使用的是服务器的地址。connect() 成功时返回 0,出错时返回 -1。        

        

4.2 封装 TCP socket         

//tcp_socket.hpp#pragma once
#include <stdio.h>        // 引入标准输入输出库
#include <string.h>       // 字符串处理函数
#include <stdlib.h>       // 一些常用函数的库(例如动态内存分配)
#include <string>         // C++ 标准库中的字符串类
#include <cassert>        // 用于断言的库
#include <unistd.h>       // 提供 POSIX 操作系统 API
#include <sys/socket.h>   // 提供 socket API
#include <netinet/in.h>   // 提供网络协议族相关定义
#include <arpa/inet.h>    // 提供 IP 地址转换相关函数
#include <fcntl.h>        // 提供文件控制接口
typedef struct sockaddr sockaddr;           // 使用别名 sockaddr
typedef struct sockaddr_in sockaddr_in;     // 使用别名 sockaddr_in#define CHECK_RET(exp) if (!(exp)) {\
return false;\
}   // 宏定义:简化返回 false 的错误检查,若表达式失败则返回 falseclass TcpSocket {
public:// 默认构造函数,初始化文件描述符为 -1TcpSocket() : fd_(-1) { }// 构造函数,传入已有的文件描述符TcpSocket(int fd) : fd_(fd) { }// 创建套接字,返回是否成功bool Socket() {fd_ = socket(AF_INET, SOCK_STREAM, 0); // 创建一个 TCP socketif (fd_ < 0) {perror("socket");    // 打印错误信息return false;}printf("open fd = %d\n", fd_);return true;}// 关闭当前套接字bool Close() const {close(fd_);    // 关闭文件描述符printf("close fd = %d\n", fd_);return true;}// 将套接字绑定到指定 IP 和端口bool Bind(const std::string& ip, uint16_t port) const {sockaddr_in addr;addr.sin_family = AF_INET;   // 设置地址族为 IPv4addr.sin_addr.s_addr = inet_addr(ip.c_str());  // 转换 IP 地址字符串为二进制addr.sin_port = htons(port); // 转换端口为网络字节序int ret = bind(fd_, (sockaddr*)&addr, sizeof(addr)); // 绑定if (ret < 0) {perror("bind");  // 打印错误信息return false;}return true;}// 监听端口,等待连接bool Listen(int num) const {int ret = listen(fd_, num);  // 设置监听队列的大小if (ret < 0) {perror("listen"); // 打印错误信息return false;}return true;}// 接受一个连接请求,返回新的套接字和连接客户端的 IP 和端口bool Accept(TcpSocket* peer, std::string* ip = NULL, uint16_t* port = NULL) const {sockaddr_in peer_addr;socklen_t len = sizeof(peer_addr);int new_sock = accept(fd_, (sockaddr*)&peer_addr, &len); // 接受连接if (new_sock < 0) {perror("accept");  // 打印错误信息return false;}printf("accept fd = %d\n", new_sock);peer->fd_ = new_sock;   // 设置新的套接字if (ip != NULL) {*ip = inet_ntoa(peer_addr.sin_addr);  // 获取客户端 IP 地址}if (port != NULL) {*port = ntohs(peer_addr.sin_port);    // 获取客户端端口号}return true;}// 接收数据并将其存入字符串缓冲区bool Recv(std::string* buf) const {buf->clear();  // 清空原有内容char tmp[1024 * 10] = { 0 }; // 临时缓冲区ssize_t read_size = recv(fd_, tmp, sizeof(tmp), 0);  // 接收数据if (read_size < 0) {perror("recv");  // 打印错误信息return false;}if (read_size == 0) {return false;  // 客户端关闭连接}buf->assign(tmp, read_size); // 将接收到的数据赋给字符串return true;}// 发送数据bool Send(const std::string& buf) const {ssize_t write_size = send(fd_, buf.data(), buf.size(), 0); // 发送数据if (write_size < 0) {perror("send");  // 打印错误信息return false;}return true;}// 连接到指定的 IP 和端口bool Connect(const std::string& ip, uint16_t port) const {sockaddr_in addr;addr.sin_family = AF_INET;    // 设置地址族为 IPv4addr.sin_addr.s_addr = inet_addr(ip.c_str());   // 转换 IP 地址字符串为二进制addr.sin_port = htons(port);  // 转换端口为网络字节序int ret = connect(fd_, (sockaddr*)&addr, sizeof(addr)); // 连接到服务器if (ret < 0) {perror("connect");  // 打印错误信息return false;}return true;}// 获取当前套接字的文件描述符int GetFd() const {return fd_;}private:int fd_;  // 套接字的文件描述符
};

4.3 TCP通用服务器       

//tcp_server.hpp#pragma once
#include <functional>
#include "tcp_socket.hpp"// 定义一个 Handler 类型,用于处理客户端请求和生成响应
typedef std::function<void(const std::string& req, std::string* resp)> Handler;class TcpServer {
public:// 构造函数,初始化服务器的 IP 和端口TcpServer(const std::string& ip, uint16_t port) : ip_(ip), port_(port) {}// 启动服务器,处理客户端请求bool Start(Handler handler) {// 1. 创建监听用的 socketCHECK_RET(listen_sock_.Socket());// 2. 绑定服务器 IP 和端口到监听 socketCHECK_RET(listen_sock_.Bind(ip_, port_));// 3. 设置监听队列大小为 5CHECK_RET(listen_sock_.Listen(5));// 4. 进入事件循环,不断接受客户端连接for (;;) {// 5. 等待并接受客户端的连接TcpSocket new_sock;std::string ip;uint16_t port = 0;if (!listen_sock_.Accept(&new_sock, &ip, &port)) {continue;  // 接受失败则跳过}// 输出客户端连接信息printf("[client %s:%d] connect!\n", ip.c_str(), port);// 6. 进入与客户端的读写循环for (;;) {std::string req;// 7. 从客户端接收请求数据,若失败则断开连接bool ret = new_sock.Recv(&req);if (!ret) {printf("[client %s:%d] disconnect!\n", ip.c_str(), port);// [注意!] 客户端断开连接时需要关闭与客户端的 socketnew_sock.Close();break;  // 退出循环,处理下一个客户端}// 8. 处理请求并生成响应std::string resp;handler(req, &resp);// 9. 将响应数据发送回客户端new_sock.Send(resp);// 输出请求和响应数据printf("[%s:%d] req: %s, resp: %s\n", ip.c_str(), port, req.c_str(), resp.c_str());}}return true;  // 启动成功}private:TcpSocket listen_sock_;  // 监听用的 TcpSocket 对象std::string ip_;         // 服务器 IP 地址uint64_t port_;          // 服务器端口号
};

4.4 英译汉服务器       

#include <unordered_map>
#include "tcp_server.hpp"// 全局字典,存储词汇与翻译的映射
std::unordered_map<std::string, std::string> g_dict;// 处理翻译请求的函数
void Translate(const std::string& req, std::string* resp) {// 在字典中查找请求的词汇auto it = g_dict.find(req);// 如果没有找到对应的翻译,返回 "未找到"if (it == g_dict.end()) {*resp = "未找到";return;}// 找到翻译结果,返回对应的翻译*resp = it->second;return;
}int main(int argc, char* argv[]) {// 检查命令行参数是否正确if (argc != 3) {printf("Usage ./dict_server [ip] [port]\n");return 1;}// 1. 初始化词典// 插入一些词汇和它们的翻译到字典中g_dict.insert(std::make_pair("hello", "你好"));  g_dict.insert(std::make_pair("world", "世界"));  g_dict.insert(std::make_pair("code", "代码"));  g_dict.insert(std::make_pair("project", "对象"));  // 2. 启动服务器// 创建 TcpServer 对象并启动服务器,监听指定的 IP 和端口TcpServer server(argv[1], atoi(argv[2]));server.Start(Translate);  // 将 Translate 函数作为请求处理函数传递给服务器return 0;
}

4.5 TCP通用客户端

// tcp_client.hpp#pragma once
#include "tcp_socket.hpp"// TcpClient 类用于封装 TCP 客户端的功能,包括连接服务器、发送和接收数据等
class TcpClient {
public:// 构造函数,初始化 TcpClient 对象并设置服务器的 IP 和端口// 注意:在调用 Connect() 之前必须先创建 socketTcpClient(const std::string& ip, uint16_t port) : ip_(ip), port_(port) {// [注意!!] 需要先创建好 socketsock_.Socket();  // 创建 socket,准备连接}// 析构函数,在对象销毁时关闭 socket~TcpClient() {sock_.Close();  // 关闭 socket 连接,释放资源}// 连接到指定的服务器// @return 如果连接成功,返回 true;否则返回 falsebool Connect() {return sock_.Connect(ip_, port_);  // 尝试连接到指定的 IP 和端口}// 从服务器接收数据// @param buf 用于存储接收到的数据的字符串缓冲区// @return 如果接收成功,返回 true;否则返回 falsebool Recv(std::string* buf) {return sock_.Recv(buf);  // 调用 TcpSocket 的 Recv 方法接收数据}// 向服务器发送数据// @param buf 要发送的数据内容// @return 如果发送成功,返回 true;否则返回 falsebool Send(const std::string& buf) {return sock_.Send(buf);  // 调用 TcpSocket 的 Send 方法发送数据}private:TcpSocket sock_;  // TcpSocket 对象,用于管理实际的 TCP 连接std::string ip_;  // 服务器的 IP 地址uint16_t port_;   // 服务器的端口号
};

4.6 英译汉客户端       

// dict_client.cc#include "tcp_client.hpp"
#include <iostream>int main(int argc, char* argv[]) {// 判断命令行参数是否正确// 程序需要接受两个参数:服务器的 IP 地址 和 端口号if (argc != 3) {// 参数错误时,输出用法提示printf("Usage ./dict_client [ip] [port]\n");return 1;  // 返回 1 表示出错}// 创建一个 TcpClient 对象,并使用命令行参数传递的 IP 和端口初始化TcpClient client(argv[1], atoi(argv[2]));// 尝试连接到服务器bool ret = client.Connect();if (!ret) {// 如果连接失败,直接退出程序return 1;}// 进入查询循环,不断请求用户输入查询的单词for (;;) {std::cout << "请输入要查询的单词:" << std::endl;std::string word;std::cin >> word;  // 获取用户输入的单词if (!std::cin) {// 如果输入流出错,退出循环break;}// 向服务器发送查询的单词client.Send(word);// 接收服务器返回的查询结果std::string result;client.Recv(&result);// 输出服务器返回的结果std::cout << result << std::endl;}return 0;  // 程序正常结束
}

        由于客户端的端口号由内核自动分配,因此通常不需要调用 bind() 来固定端口号。需要注意的是,客户端并非不能调用 bind(),而是没有必要固定端口号。若客户端调用了 bind() 并指定端口号,可能会导致在同一台机器上启动多个客户端时,端口号冲突,进而无法正常建立连接。

        对于服务器来说,bind() 也不是必须调用的。如果服务器不调用 bind(),内核会自动分配一个监听端口。不过,若服务器不指定端口,每次启动时分配的端口号都会不同,这可能会给客户端连接带来困难。

        


五、其他补充        

        在测试多个连接时,当启动第二个客户端尝试连接服务器时,发现第二个客户端无法与服务器正常通信。分析原因是,服务器在接收到一个请求后,进入了一个 while 循环,不断尝试读取数据,而没有继续调用 accept() 来接收新的连接请求。因此,服务器无法接受新的连接。目前的 TCP 实现只能处理一个连接,这是不合理的,无法满足同时处理多个连接的需求。

5.1 多进程版本的TCP程序 

        通过每个请求, 创建子进程的方式来支持多连接      

//tcp_process_server.hpp#pragma once
#include <functional>
#include <signal.h>
#include "tcp_socket.hpp"
typedef std::function<void(const std::string& req, std::string* resp)> Handler;
// 多进程版本的 Tcp 服务器
class TcpProcessServer {
public:TcpProcessServer(const std::string& ip, uint16_t port) : ip_(ip), port_(port) {// 需要处理子进程signal(SIGCHLD, SIG_IGN);}void ProcessConnect(const TcpSocket& new_sock, const std::string& ip, uint16_t port,Handler handler) {int ret = fork();if (ret > 0) {// father// 父进程不需要做额外的操作, 直接返回即可.// 思考, 这里能否使用 wait 进行进程等待?// 如果使用 wait , 会导致父进程不能快速再次调用到 accept, 仍然没法处理多个请求// [注意!!] 父进程需要关闭 new_socknew_sock.Close();return;}else if (ret == 0) {// child// 处理具体的连接过程. 每个连接一个子进程for (;;) {std::string req;bool ret = new_sock.Recv(&req);if (!ret) {// 当前的请求处理完了, 可以退出子进程了. 注意, socket 的关闭在析构函数中就完成了printf("[client %s:%d] disconnected!\n", ip.c_str(), port);exit(0);}std::string resp;handler(req, &resp);new_sock.Send(resp);printf("[client %s:%d] req: %s, resp: %s\n", ip.c_str(), port,req.c_str(), resp.c_str());}}else {perror("fork");}}bool Start(Handler handler) {// 1. 创建 socket;CHECK_RET(listen_sock_.Socket());// 2. 绑定端口号CHECK_RET(listen_sock_.Bind(ip_, port_));// 3. 进行监听CHECK_RET(listen_sock_.Listen(5));// 4. 进入事件循环for (;;) {// 5. 进行 acceptTcpSocket new_sock;std::string ip;uint16_t port = 0;if (!listen_sock_.Accept(&new_sock, &ip, &port)) {continue;}printf("[client %s:%d] connect!\n", ip.c_str(), port);ProcessConnect(new_sock, ip, port, handler);}return true;}
private:TcpSocket listen_sock_;std::string ip_;uint64_t port_;
};

        

5.2 多线程版本的TCP程序

        通过每个请求, 创建一个线程的方式来支持多连接        

//tcp_thread_server.hpp#pragma once
#include <functional>
#include <pthread.h>
#include "tcp_socket.hpp"
typedef std::function<void(const std::string&, std::string*)> Handler;
struct ThreadArg {TcpSocket new_sock;std::string ip;uint16_t port;Handler handler;
};
class TcpThreadServer {
public:TcpThreadServer(const std::string& ip, uint16_t port) : ip_(ip), port_(port) {}bool Start(Handler handler) {// 1. 创建 socket;CHECK_RET(listen_sock_.Socket());// 2. 绑定端口号CHECK_RET(listen_sock_.Bind(ip_, port_));// 3. 进行监听CHECK_RET(listen_sock_.Listen(5));// 4. 进入循环for (;;) {// 5. 进行 acceptThreadArg* arg = new ThreadArg();arg->handler = handler;bool ret = listen_sock_.Accept(&arg->new_sock, &arg->ip, &arg->port);if (!ret) {continue;}printf("[client %s:%d] connect\n", arg->ip.c_str(), arg->port);// 6. 创建新的线程完成具体操作pthread_t tid;pthread_create(&tid, NULL, ThreadEntry, arg);pthread_detach(tid);}return true;}// 这里的成员函数为啥非得是 static?static void* ThreadEntry(void* arg) {// C++ 的四种类型转换都是什么?ThreadArg* p = reinterpret_cast<ThreadArg*>(arg);ProcessConnect(p);// 一定要记得释放内存!!! 也要记得关闭文件描述符p->new_sock.Close();delete p;return NULL;}// 处理单次连接. 这个函数也得是 staticstatic void ProcessConnect(ThreadArg* arg) {// 1. 循环进行读写for (;;) {std::string req;// 2. 读取请求bool ret = arg->new_sock.Recv(&req);if (!ret) {printf("[client %s:%d] disconnected!\n", arg->ip.c_str(), arg->port);break;}std::string resp;// 3. 根据请求计算响应arg->handler(req, &resp);// 4. 发送响应arg->new_sock.Send(resp);printf("[client %s:%d] req: %s, resp: %s\n", arg->ip.c_str(),arg->port, req.c_str(), resp.c_str());}}
private:TcpSocket listen_sock_;std::string ip_;uint16_t port_;
};

        

5.3 线程池版本的 TCP 服务器

// tcp_thread_pool_server.hpp#pragma once
#include <functional>
#include <pthread.h>
#include <queue>
#include <vector>
#include <mutex>
#include <condition_variable>
#include "tcp_socket.hpp"typedef std::function<void(const std::string&, std::string*)> Handler;struct ThreadArg {TcpSocket new_sock;std::string ip;uint16_t port;Handler handler;
};class ThreadPool {
public:ThreadPool(size_t numThreads) {stopFlag_ = false;for (size_t i = 0; i < numThreads; ++i) {threads_.emplace_back(&ThreadPool::Worker, this);}}~ThreadPool() {{std::lock_guard<std::mutex> lock(mutex_);stopFlag_ = true;}condVar_.notify_all();for (auto& thread : threads_) {if (thread.joinable()) {thread.join();}}}void SubmitTask(std::function<void()> task) {{std::lock_guard<std::mutex> lock(mutex_);taskQueue_.push(task);}condVar_.notify_one();}private:void Worker() {while (true) {std::function<void()> task;{std::unique_lock<std::mutex> lock(mutex_);condVar_.wait(lock, [this] { return stopFlag_ || !taskQueue_.empty(); });if (stopFlag_ && taskQueue_.empty()) {return;}task = taskQueue_.front();taskQueue_.pop();}task();}}std::vector<std::thread> threads_;std::queue<std::function<void()>> taskQueue_;std::mutex mutex_;std::condition_variable condVar_;bool stopFlag_;
};class TcpThreadPoolServer {
public:TcpThreadPoolServer(const std::string& ip, uint16_t port, size_t numThreads): ip_(ip), port_(port), threadPool_(numThreads) {}bool Start(Handler handler) {// 1. 创建 socket;CHECK_RET(listen_sock_.Socket());// 2. 绑定端口号CHECK_RET(listen_sock_.Bind(ip_, port_));// 3. 进行监听CHECK_RET(listen_sock_.Listen(5));// 4. 进入循环for (;;) {// 5. 进行 acceptThreadArg* arg = new ThreadArg();arg->handler = handler;bool ret = listen_sock_.Accept(&arg->new_sock, &arg->ip, &arg->port);if (!ret) {continue;}printf("[client %s:%d] connect\n", arg->ip.c_str(), arg->port);// 6. 将任务提交给线程池处理threadPool_.SubmitTask([arg]() {ProcessConnect(arg);// 一定要记得释放内存!!! 也要记得关闭文件描述符arg->new_sock.Close();delete arg;});}return true;}// 处理单次连接static void ProcessConnect(ThreadArg* arg) {// 1. 循环进行读写for (;;) {std::string req;// 2. 读取请求bool ret = arg->new_sock.Recv(&req);if (!ret) {printf("[client %s:%d] disconnected!\n", arg->ip.c_str(), arg->port);break;}std::string resp;// 3. 根据请求计算响应arg->handler(req, &resp);// 4. 发送响应arg->new_sock.Send(resp);printf("[client %s:%d] req: %s, resp: %s\n", arg->ip.c_str(),arg->port, req.c_str(), resp.c_str());}}private:TcpSocket listen_sock_;std::string ip_;uint16_t port_;ThreadPool threadPool_;  // 线程池对象
};

        

5.4 了解TCP协议通讯流程        

服务器初始化 :

  • 调用socket, 创建文件描述符;
  • 调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失败;
  • 调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备;
  • 调用accecpt, 并阻塞, 等待客户端连接过来;

        
建立连接的过程 : 三次握手

  • 调用socket, 创建文件描述符;
  • 调用connect, 向服务器发起连接请求;
  • connect会发出SYN段并阻塞等待服务器应答; (第一次)
  • 服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)
  • 客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)
     

数据传输的过程 :

  • 建立连接后,TCP协议提供全双工的通信服务; 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方可以同时写数据; 相对的概念叫做半双工, 同一条连接在同一时刻, 只能由一方来写数据;
  • 服务器从accept()返回后立刻调 用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等待;
  • 这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期间客户端调用read()阻塞等待服务器的应答;
  • 服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求;
  • 客户端收到后从read()返回, 发送下一条请求,如此循环下去;

断开连接的过程 : 四次挥手

  • 如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次);
  • 此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次);
  • read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN; (第三次)
  • 客户端收到FIN, 再返回一个ACK给服务器; (第四次)

        

        在学习Socket API时,需关注应用程序与TCP协议层的交互。具体而言,当应用程序调用某个Socket函数时,TCP协议层会执行相应的动作。例如,调用connect()时,TCP协议层会发送SYN段;而应用程序通过Socket函数的返回值来了解TCP协议层的状态变化。比如,若某个阻塞的Socket函数返回,则表示TCP协议层已接收到特定的数据段;又如,当read()函数返回0时,表明TCP协议层已收到FIN段。

        


版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词