Socket编程--TCP

📅 2026/8/20 15:34:24
Socket编程--TCP
一、TCP socket APITCP编程与前面的UDP编程一样都属于网络通信所以也需要包含下面三个网络通信必备的头文件第一个接口socket()用于创建套接字_sockfd socket(AF_INET, SOCK_STREAM, 0);与前面的UDP不同的地方在于由于TCP协议是面向字节流的所以类型type要采用SOCK_STREAM。第二个接口bind()用于把socket文件描述符与本机地址进行绑定int n bind(_listensockfd, (struct sockaddr *)local, sizeof(local));由于TCP是面向连接的所以TCP编程需要比UDP多一个环节就是客户端需要向服务器端发起连接请求。也就是说服务器端需要处于一种监听的状态叫做listen便于收到客户端的连接请求。所以也就需要使用到第三个接口listenlisten的返回值listen接口的作用就是设置套接字sockfd为监听状态。此接口中的backlog表示的是链接队列的长度即链接队列最大容纳连接数。它是一个整型数值如 10、128。这里我们将链接数量设置为16static const int gbacklog 16; n listen(_listensockfd, gbacklog);当完成这些工作后就可以启动TCP服务了。查看我们所启动的TCP服务可以使用指令netstate -antp。-a表示查看所有的信息-n表示1用数字显示-t表示显示的是tcp的内容-u表示的则是upd的内容-p表示将进程信息带上。在完成了上述的工作之后我们接下来需要完成服务器的启动start()与UDP不同的是由于TCP是面向连接的所以不能一上来就读取客户端的信息先要做的是获取客户端的连接请求。获取连接我们需要使用的接口是accept()上面这两个参数合起来可以帮助我们获取到客户端的socket ipport信息。struct sockaddr_in clientaddr; socklen_t len sizeof(clientaddr); int sockfd accept(_listensockfd, (struct sockaddr *)clientaddr, len);这一步操作其实类似于UDP中recvfrom接口的作用都是用于获取客户端的ip和端口号信息。当获取失败依旧是返回-1但是当返回成功时该接口将返回一个文件描述符。而在TCP中与UDP不同的是会存在很多的文件描述符而我们之前所创建的文件描述符叫做listen_socket监听套接字。未来返回的套接字叫做IO_socket即服务套接字。未来一个服务套件字代表一条连接1accept()用于接收连接是服务器端对连接的操作而用于客户端发起连接请求的接口是--connect()连接成功返回0失败返回-1。在进行地址转换时会使用下面两个接口——inet_ntopinet_ptoninet_ntop这个接口的作用是把二进制网络字节序IP 地址转换成人可读的点分字符串格式int af地址族AF_INET代表 IPv4AF_INET6代表 IPv6const void *src输入二进制 IP网络字节序IPv4就传(sockaddr_in)-sin_addrchar *dst输出缓冲区转换完成的 IP 字符串会写到这里由调用者自己提供内存socklen_t sizedst 缓冲区字节大小防止缓冲区溢出inet_pton这个接口的作用是把点分字符串格式IP 地址转换成二进制网络字节序IP地址。int af地址族AF_INET(IPv4) / AF_INET6(IPv6)const char *src输入IP 字符串如 127.0.0.1void *dst输出缓冲区存放二进制网络序 IP。IPv4 传addr.sin_addr二、version0单进程版本version0仅用于做接口的说明和熟悉后续所要用的接口在成功获取到连接后我们就可以处理新的socket套接字了InetAddr clientaddress(clientaddr); serviceIO(sockfd, clientaddress);TCP和UDP一样在进行通信时都是全双工的void serviceIO(int socket,InetAddr address) { LOG(LogLevel::DEBUG) client info is: address.ToString(); while(true) { char inbuffer[1024] {0}; ssize_t n read(sockfd, inbuffer, sizeof(inbuffer) - 1);//先读后写入 if (n 0) { inbuffer[n] 0; LOG(LogLevel::INFO) address.ToString() say# inbuffer; std::string echo_string server echo# ;//新建一个新字符串用于接收客户端的信息 echo_string inbuffer; write(sockfd, echo_string.c_str(), echo_string.size());//写入 } else if (n 0)//读到0说明连接断开了 { LOG(LogLevel::INFO) client quit, address: address.ToString(); break; } else { LOG(LogLevel::ERROR) client read error, address: address.ToString(); break; } } }version0服务器端完整代码如下EchoTcpServer.hpp(version0)#pragma once #include iostream #include string #include cstring #include functional #include unistd.h #include sys/socket.h #include arpa/inet.h #include netinet/in.h #include sys/wait.h // #include signal.h #include pthread.h #include InetAddr.hpp #include Logger.hpp #include ThreadPool.hpp using namespace NS_LOG_MODULE; using namespace NS_THREAD_POOL_MODULE; using task_t std::functionvoid(); enum { SUCCESS 0, USAGE_ERR, SOCKET_ERR, BIND_ERR, LISTEN_ERR }; static const int gbacklog 16; static const uint16_t gport 8888; class TcpServer { public: TcpServer(uint16_t port gport) : _port(port) { } void InitServer() { // 1. 创建socket _listensockfd socket(AF_INET, SOCK_STREAM, 0); // TCP if (_listensockfd 0) { LOG(LogLevel::FATAL) create socket error; exit(SOCKET_ERR); } LOG(LogLevel::DEBUG) create socket success: _listensockfd; // 2. 填充本地socket信息 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; // 任意地址bind // 3. bind int n bind(_listensockfd, (struct sockaddr *)local, sizeof(local)); if (n 0) { LOG(LogLevel::FATAL) bind error; exit(BIND_ERR); } LOG(LogLevel::DEBUG) bind socket success; // 4. tcp是面向连接的所以TCP服务器要处于一种叫做listen监听状态。 n listen(_listensockfd, gbacklog); if (n 0) { LOG(LogLevel::FATAL) listen error; exit(LISTEN_ERR); } LOG(LogLevel::DEBUG) listen socket success; } void serviceIO(int socket,InetAddr address) { LOG(LogLevel::DEBUG) client info is: address.ToString(); while(true) { char inbuffer[1024] {0}; ssize_t n read(sockfd, inbuffer, sizeof(inbuffer) - 1);//先读后写入 if (n 0) { inbuffer[n] 0; LOG(LogLevel::INFO) address.ToString() say# inbuffer; std::string echo_string server echo# ;//新建一个新字符串用于接收客户端的信息 echo_string inbuffer; write(sockfd, echo_string.c_str(), echo_string.size());//写入 } else if (n 0)//读到0说明连接断开了 { LOG(LogLevel::INFO) client quit, address: address.ToString(); break; } else { LOG(LogLevel::ERROR) client read error, address: address.ToString(); break; } } } void Start() { while (true) { struct sockaddr_in clientaddr; socklen_t len sizeof(clientaddr); // 5. 获取连接 int sockfd accept(_listensockfd, (struct sockaddr *)clientaddr, len); if (sockfd 0) { LOG(LogLevel::WARNING) accept errr!; continue; } LOG(LogLevel::DEBUG) accept success, sockfd: sockfd; //6. 处理新sockfd version 0 InetAddr clientaddress(clientaddr); serviceIO(sockfd, clientaddress); } ~TcpServer() { close(_listensockfd); } private: uint16_t _port; // 不需要显示包含ip int _listensockfd; };TCP的客户端与UDP的客户端的逻辑一模一样要访问目标服务器首先需要知道服务器端的ip// ./client_tcp server_ip server_port int main(int argc, char *argv[]) { if (argc ! 3) { Usage(argv[0]); exit(1); } std::string server_ip argv[1]; uint16_t server_port std::stoi(argv[2]);接下来就是创建客户端的tcp socket套接字// 1. 创建tcpsocket套接字 int sockfd socket(AF_INET, SOCK_STREAM, 0);//AF_INET:网络通信服务,SOCK_STREAM:流式套接 if (sockfd 0) { std::cerr socket error std::endl; exit(2); }然后是进行绑定操作bind。无论是UPD还是TCP都需要绑定自己的ip号和端口号。但是都不需要自己手动地对客户端进行绑定操作系统会自动为我们分配随机端口防止端口号冲突但是TCP在这里比UDP多一步操作由于TCP是面向连接的所以在bind端口号和ip过后需要发起建立连接的请求所以我们在这里需要使用到接口connect()。需要注意的是connect()接口在建立连接时会自动帮我们进行绑定也就是说此处只需要只用到connect接口即可// 2. bind端口号ip发起建立连接的工作 InetAddr serveraddress(server_port, server_ip);//构建好对象后InetAddr中会自动转换为网络结构 int n connect(sockfd, (struct sockaddr *)serveraddress.GetNetAddress(), serveraddress.Len()); if (n 0) { std::cerr connect to serveraddress.ToString() failed! std::endl; exit(3); } std::cerr connect to serveraddress.ToString() success! std::endl;连接完成后就可以进行通信操作了// 3. 通信 while(true) { std::string line; std::cout please Enter# ; std::getline(std::cin, line); write(sockfd, line.c_str(), line.size()); char inbuffer[1024]; ssize_t n read(sockfd, inbuffer, sizeof(inbuffer));//同一个套接字既可以读又可以写证明TCP通信是全双工的。 if(n 0) { inbuffer[n] 0; std::cout inbuffer std::endl; } else if(n 0) { std::cout read enf of file! std::endl; break; } else { std::cerr read error! std::endl; break; } }客户端完整代码如下EchoTcpClient.cc#include iostream #include string #include cstdlib #include unistd.h #include sys/socket.h #include arpa/inet.h #include netinet/in.h #include InetAddr.hpp static void Usage(const std::string name) { std::cerr Usage:\n\t; std::cerr name server_ip server_port std::endl; } // ./client_tcp server_ip server_port int main(int argc, char *argv[]) { if (argc ! 3) { Usage(argv[0]); exit(1); } std::string server_ip argv[1]; uint16_t server_port std::stoi(argv[2]); // 1. 创建tcpsocket套接字 int sockfd socket(AF_INET, SOCK_STREAM, 0); if (sockfd 0) { std::cerr socket error std::endl; exit(2); } // 2. bind端口号ip发起建立连接的工作 // {client ip, client port} - {server ip, server port} InetAddr serveraddress(server_port, server_ip); int n connect(sockfd, (struct sockaddr *)serveraddress.GetNetAddress(), serveraddress.Len()); if (n 0) { std::cerr connect to serveraddress.ToString() failed! std::endl; exit(3); } std::cerr connect to serveraddress.ToString() success! std::endl; // 3. 通信 while(true) { std::string line; std::cout please Enter# ; std::getline(std::cin, line); write(sockfd, line.c_str(), line.size()); char inbuffer[1024]; ssize_t n read(sockfd, inbuffer, sizeof(inbuffer)); if(n 0) { inbuffer[n] 0; std::cout inbuffer std::endl; } else if(n 0) { std::cout read enf of file! std::endl; break; } else { std::cerr read error! std::endl; break; } } return 0; }EchoTcpServerMain.cc#include EchoTcpServer.hpp #include memory static void Usage(const std::string process) { std::cerr Usage:\n\t; std::cerr process local_port std::endl; } int main(int argc, char *argv[]) { if (argc ! 2) { Usage(argv[0]); exit(USAGE_ERR); } ENABLE_CONSOLE_LOG_STRATEGY(); uint16_t server_port std::stoi(argv[1]); std::unique_ptrTcpServer tsvr std::make_uniqueTcpServer(server_port); tsvr-InitServer(); tsvr-Start(); return 0; }编译运行后启动服务器端和客户端再输入命令netstate -antp查看所启动的TCP服务会发现server_tc和client_tc已经建立起了对应的连接。一条是客户端向服务器端建立的连接一条是服务器端向客户端建立的连接。由于是在一台机器上启动的服务器端和客户端所以相当于一台机器上会看到两个不同视角的连接一个是客户端的一个是服务器端的。但是上述version0是一个单进程的TCP服务当一个客户端与服务器端建立起连接后如果客户端一直不退出那么进程就会阻塞在读写那部分逻辑中也就是其他的客户端无法与服务器建立起连接服务器无法并发地处理多个客户端发出的请求。这显然与当代网络通信的要求不符。三、version1多进程版本为了解决上述问题需要在version0的基础上进行改造进而引申出版本version1。单进程的服务器对客户端请求的处理显然行不通。于是有了version1版本引入了父子进程。由父进程负责对客户端的请求的接收然后将这些接收到的请求交给子进程让子进程进程处理IO。如果创建子进程失败返回-1。如果创建子进程失败则后面的操作都无法进行下去所以直接让服务器进程终止。pid_t id fork(); if(id 0) { LOG(LogLevel::FATAL) fork errr!; exit(FORK_ERR); }创建进程成功后让子进程进行IO任务的处理else if(id 0) { InetAddr clientaddress(clientaddr); serviceIO(sockfd, clientaddress); close(sockfd); exit(0); // 终止子进程 }当子进程完成任务之后由于fork之后的代码是共享的如果子进程不退出就有可能进执行父进程获取连接部分的代码这样的情况显然不合理。所有当子进程完成任务之后我们要直接终止子进程防止影响后续父进程的代码执行。但是如果终止了子进程那子进程就进入了僵尸状态。在前面的Linux进程中我们提到过子进程终止需要父进程wait去回收子进程。但是如果按照这个逻辑来完成这部分的代码代码的执行不就又变成串行了吗父进程需要等待子进程退出回收子进程后才能执行后续任务。或者采用在前面学到过的waitpid中的WNHANG信号但是这种方法的调用时机、回收范围不对依旧产生僵尸进程从而产生bug。所以此处的最佳实践是加上signal(SIGCHLD, SIG_IGN)子进程一退出会自动释放无需父进程等待回收子进程。在此处也有另外一种方式那就是子进程再fork一次创建孙子进程然后子进程直接退出让孙子进程来执行后续的IO任务。而由于子进程退出了孙子进程就变成孤儿进程了。这是利用系统特性的写法。父进程成功fork后创建出子进程父进程的文件描述符表也要给子进程拷贝一份。但是在后续子进程的任务中并没有使用到监听套接字(listen_socket)。在网络通信中有一个大的原则那就是如果有一个资源不再使用了那就需要尽早释放掉。文件描述符是有用且有限的资源由于子进程在后续不再使用这部分资源就需要及时释放掉。同理父进程只负责接收客户端的信号所以不需要使用到服务套接字(IO_socket)所以也需要及时关闭这部分套接字else if(id 0) { // 关闭掉自己不需要的sockfd close(_listensockfd); // 子进程 if(fork() 0) exit(0); // 子进程直接退出 // 孙子进程 - 孤儿进程 InetAddr clientaddress(clientaddr); serviceIO(sockfd, clientaddress); close(sockfd); exit(0); // 终止子进程 } else { // 父进程 // wait(id); waitpid: WNOHANG // 关闭掉自己不需要的sockfd close(sockfd); pid_t rid waitpid(id, nullptr, 0); (void)rid; }version1服务器端完整代码#pragma once #include iostream #include string #include cstring #include functional #include unistd.h #include sys/socket.h #include arpa/inet.h #include netinet/in.h #include sys/wait.h // #include signal.h #include pthread.h #include InetAddr.hpp #include Logger.hpp #include ThreadPool.hpp using namespace NS_LOG_MODULE; using namespace NS_THREAD_POOL_MODULE; using task_t std::functionvoid(); enum { SUCCESS 0, USAGE_ERR, SOCKET_ERR, BIND_ERR, LISTEN_ERR, FORK_ERR }; static const int gbacklog 16; static const uint16_t gport 8888; class TcpServer { public: TcpServer(uint16_t port gport) : _port(port) { } void InitServer() { // 1. 创建socket _listensockfd socket(AF_INET, SOCK_STREAM, 0); // TCP if (_listensockfd 0) { LOG(LogLevel::FATAL) create socket error; exit(SOCKET_ERR); } LOG(LogLevel::DEBUG) create socket success: _listensockfd; // 2. 填充本地socket信息 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; // 任意地址bind // 3. bind int n bind(_listensockfd, (struct sockaddr *)local, sizeof(local)); if (n 0) { LOG(LogLevel::FATAL) bind error; exit(BIND_ERR); } LOG(LogLevel::DEBUG) bind socket success; // 4. tcp是面向连接的所以TCP服务器要处于一种叫做listen监听状态。 n listen(_listensockfd, gbacklog); if (n 0) { LOG(LogLevel::FATAL) listen error; exit(LISTEN_ERR); } LOG(LogLevel::DEBUG) listen socket success; } void serviceIO(int socket,InetAddr address) { LOG(LogLevel::DEBUG) client info is: address.ToString(); while(true) { char inbuffer[1024] {0}; ssize_t n read(sockfd, inbuffer, sizeof(inbuffer) - 1);//先读后写入 if (n 0) { inbuffer[n] 0; LOG(LogLevel::INFO) address.ToString() say# inbuffer; std::string echo_string server echo# ;//新建一个新字符串用于接收客户端的信息 echo_string inbuffer; write(sockfd, echo_string.c_str(), echo_string.size());//写入 } else if (n 0)//读到0说明连接断开了 { LOG(LogLevel::INFO) client quit, address: address.ToString(); break; } else { LOG(LogLevel::ERROR) client read error, address: address.ToString(); break; } } } void Start() { while (true) { struct sockaddr_in clientaddr; socklen_t len sizeof(clientaddr); // 5. 获取连接 int sockfd accept(_listensockfd, (struct sockaddr *)clientaddr, len); if (sockfd 0) { LOG(LogLevel::WARNING) accept errr!; continue; } LOG(LogLevel::DEBUG) accept success, sockfd: sockfd; //version1:多进程版本 pid_t id fork(); if(id 0) { LOG(LogLevel::FATAL) fork errr!; exit(FORK_ERR); } else if(id 0) { // 关闭掉自己不需要的sockfd close(_listensockfd); // 子进程 if(fork() 0) exit(0); // 子进程直接退出 // 孙子进程 - 孤儿进程 InetAddr clientaddress(clientaddr); serviceIO(sockfd, clientaddress); close(sockfd); exit(0); // 重要 } else{ // 父进程 // wait(id); waitpid: WNOHANG // 关闭掉自己不需要的sockfd close(sockfd); pid_t rid waitpid(id, nullptr, 0); (void)rid; } } ~TcpServer() { close(_listensockfd); } private: uint16_t _port; // 不需要显示包含ip int _listensockfd; };这里介绍一个工具用于网络编程测试服务器——telnet命令格式telnet 服务IP 端口号连接成功后按下Ctrl ]调出命令行键盘输入文字回车会直接发给服务端。服务端返回的数据实时打印在终端。再按下Ctrl ]调出命令行输入quit回车将会断开连接。四、version2多线程版本在前面的version1多进程服务器中每有一个客户端就需要创建一个进程这样的话未免开销太大。所以也就延伸出了version2多线程版本。在这里我们直接使用C库封装的线程。需要包含头文件#includepthread.h//version 2:多线程版本 pthread_t tid; InetAddr clientaddress(clientaddr); ThreadData *td new ThreadData(this, sockfd, clientaddress); pthread_create(tid, nullptr, thread_routine, (void *)td);同理由于主线程也需要等待回收线程join()一旦join就会阻塞等待这样不就出现和父子进程一样的问题了吗所以我们要把线程设置为分离状态。这里的thread_routine任务函数使用static静态方法是为了避免this指针保证参数传递的正确性。但是将线程函数中无法调用ServiceIO函数因为pthread线程函数只能传一个 void* 参数而使用ServiceIO函数需要传入多个参数所以需要一个类来封装多组数据。此处不需要像前面的父子进程一样关闭不需要的套接字因为父子线程是共享文件描述符表的。class ThreadData { public: ThreadData(TcpServer *ts, int sockfd, const InetAddr addr) : _this(ts), _sockfd(sockfd), _addr(addr) { } ~ThreadData() { close(_sockfd); } public: TcpServer *_this; int _sockfd; InetAddr _addr; }; static void *thread_routine(void *args) { ThreadData *td static_castThreadData *(args); pthread_detach(pthread_self()); td-_this-serviceIO(td-_sockfd, td-_addr); delete td; return nullptr; }由于上述代码中服务器端提供的服务是长服务长连接。而多进程、多线程的模式不太适用于处理这种长服务长连接的服务大多用于小型应用的服务。一旦有大量的客户端的请求涌入服务器进程和线程数量大量增多就会增加CPU的调度成本进程和线程切换的成本增加使得服务器卡顿。所以如果要使用多进程或多线程版本的服务器我们就需要将长服务改为短服务。由此需要将代码中的长服务改为短服务就是去掉while循环的逻辑所以version2服务器端完整代码为#pragma once #include iostream #include string #include cstring #include functional #include unistd.h #include sys/socket.h #include arpa/inet.h #include netinet/in.h #include sys/wait.h // #include signal.h #include pthread.h #include InetAddr.hpp #include Logger.hpp #include ThreadPool.hpp using namespace NS_LOG_MODULE; using namespace NS_THREAD_POOL_MODULE; using task_t std::functionvoid(); enum { SUCCESS 0, USAGE_ERR, SOCKET_ERR, BIND_ERR, LISTEN_ERR, FORK_ERR }; static const int gbacklog 16; static const uint16_t gport 8888; class TcpServer { public: TcpServer(uint16_t port gport) : _port(port) { } void InitServer() { // 1. 创建socket _listensockfd socket(AF_INET, SOCK_STREAM, 0); // TCP if (_listensockfd 0) { LOG(LogLevel::FATAL) create socket error; exit(SOCKET_ERR); } LOG(LogLevel::DEBUG) create socket success: _listensockfd; // 2. 填充本地socket信息 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; // 任意地址bind // 3. bind int n bind(_listensockfd, (struct sockaddr *)local, sizeof(local)); if (n 0) { LOG(LogLevel::FATAL) bind error; exit(BIND_ERR); } LOG(LogLevel::DEBUG) bind socket success; // 4. tcp是面向连接的所以TCP服务器要处于一种叫做listen监听状态。 n listen(_listensockfd, gbacklog); if (n 0) { LOG(LogLevel::FATAL) listen error; exit(LISTEN_ERR); } LOG(LogLevel::DEBUG) listen socket success; } void serviceIO(int socket,InetAddr address) { LOG(LogLevel::DEBUG) client info is: address.ToString(); //长服务改为短服务 char inbuffer[1024] {0}; ssize_t n read(sockfd, inbuffer, sizeof(inbuffer) - 1);//先读后写入 if (n 0) { inbuffer[n] 0; LOG(LogLevel::INFO) address.ToString() say# inbuffer; std::string echo_string server echo# ;//新建一个新字符串用于接收客户端的信息 echo_string inbuffer; write(sockfd, echo_string.c_str(), echo_string.size());//写入 } else if (n 0)//读到0说明连接断开了 { LOG(LogLevel::INFO) client quit, address: address.ToString(); } else { LOG(LogLevel::ERROR) client read error, address: address.ToString(); } } void Start() { while (true) { struct sockaddr_in clientaddr; socklen_t len sizeof(clientaddr); // 5. 获取连接 int sockfd accept(_listensockfd, (struct sockaddr *)clientaddr, len); if (sockfd 0) { LOG(LogLevel::WARNING) accept errr!; continue; } LOG(LogLevel::DEBUG) accept success, sockfd: sockfd; //version 2:多线程版本 pthread_t tid; InetAddr clientaddress(clientaddr); ThreadData *td new ThreadData(this, sockfd, clientaddress); pthread_create(tid, nullptr, thread_routine, (void *)td); } } ~TcpServer() { close(_listensockfd); } private: uint16_t _port; // 不需要显示包含ip int _listensockfd; };五、version3接入线程池前面多线程的版本仍然有缺陷。原因是由于提供的服务是短服务而每来一个短服务都需要创建一个线程有可能创建线程的开销比短服务的开销还要大。所以我们可以直接接入线程池这样就省去了来了一个客户端才创建一个线程减少了大批量创建线程的开销。由此引出version3——线程池版本服务器。version3服务器端完整代码为#pragma once #include iostream #include string #include cstring #include functional #include unistd.h #include sys/socket.h #include arpa/inet.h #include netinet/in.h #include sys/wait.h // #include signal.h #include pthread.h #include InetAddr.hpp #include Logger.hpp #include ThreadPool.hpp using namespace NS_LOG_MODULE; using namespace NS_THREAD_POOL_MODULE; using task_t std::functionvoid(); enum { SUCCESS 0, USAGE_ERR, SOCKET_ERR, BIND_ERR, LISTEN_ERR, FORK_ERR }; static const int gbacklog 16; static const uint16_t gport 8888; class TcpServer { public: TcpServer(uint16_t port gport) : _port(port) { } void InitServer() { // 1. 创建socket _listensockfd socket(AF_INET, SOCK_STREAM, 0); // TCP if (_listensockfd 0) { LOG(LogLevel::FATAL) create socket error; exit(SOCKET_ERR); } LOG(LogLevel::DEBUG) create socket success: _listensockfd; // 2. 填充本地socket信息 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; // 任意地址bind // 3. bind int n bind(_listensockfd, (struct sockaddr *)local, sizeof(local)); if (n 0) { LOG(LogLevel::FATAL) bind error; exit(BIND_ERR); } LOG(LogLevel::DEBUG) bind socket success; // 4. tcp是面向连接的所以TCP服务器要处于一种叫做listen监听状态。 n listen(_listensockfd, gbacklog); if (n 0) { LOG(LogLevel::FATAL) listen error; exit(LISTEN_ERR); } LOG(LogLevel::DEBUG) listen socket success; } void serviceIO(int sockfd, InetAddr address) { // 短连接短服务 char inbuffer[1024] {0}; ssize_t n read(sockfd, inbuffer, sizeof(inbuffer) - 1); if (n 0) { inbuffer[n] 0; LOG(LogLevel::INFO) address.ToString() say# inbuffer; // 写 std::string echo_string server echo# ; echo_string inbuffer; write(sockfd, echo_string.c_str(), echo_string.size()); } else if (n 0) { LOG(LogLevel::INFO) client quit, address: address.ToString(); } else { LOG(LogLevel::ERROR) client read error, address: address.ToString(); } close(sockfd); } class ThreadData { public: ThreadData(TcpServer *ts, int sockfd, const InetAddr addr) : _this(ts), _sockfd(sockfd), _addr(addr) { } ~ThreadData() { close(_sockfd); } public: TcpServer *_this; int _sockfd; InetAddr _addr; }; static void *thread_routine(void *args) { ThreadData *td static_castThreadData *(args); pthread_detach(pthread_self()); td-_this-serviceIO(td-_sockfd, td-_addr); delete td; return nullptr; } void Start() { while (true) { struct sockaddr_in clientaddr; socklen_t len sizeof(clientaddr); // 5. 获取连接 int sockfd accept(_listensockfd, (struct sockaddr *)clientaddr, len); if (sockfd 0) { LOG(LogLevel::WARNING) accept errr!; continue; } LOG(LogLevel::DEBUG) accept success, sockfd: sockfd; // version 3: 接入线程池 InetAddr clientaddress(clientaddr); ThreadPooltask_t::Instance()-Enqueue([this, sockfd, clientaddress]()-void{ this-serviceIO(sockfd, clientaddress); }); } } ~TcpServer() { close(_listensockfd); } private: uint16_t _port; // 不需要显示包含ip int _listensockfd; };接入线程池的好处在于线程池会预先创建固定上限的工作线程不会随客户端并发请求无限制新建线程。当大量客户端同时接入、空闲线程耗尽时新任务会存入任务队列排队等待避免瞬间大批量创建线程耗尽系统资源导致服务器卡顿、崩溃。六、其他代码InetAddr.hpp#pragma once #include iostream #include string #include strings.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h // 对客户端进行先描述 class InetAddr { public: InetAddr(const struct sockaddr_in address):_address(address), _len(sizeof(address)) { _ip inet_ntoa(_address.sin_addr); _port ntohs(_address.sin_port); } InetAddr(uint16_t port, const std::string ip 0.0.0.0):_ip(ip), _port(port) { bzero(_address, sizeof(_address)); _address.sin_family AF_INET; _address.sin_port htons(_port); // h-n _address.sin_addr.s_addr inet_addr(_ip.c_str()); // 1. 字符串ip-4字节IP 2. hton _len sizeof(_address); } bool operator (const InetAddr addr) { return (this-_ip addr._ip) (this-_port addr._port); } std::string ToString() { return [ _ip : std::to_string(_port) ]; } InetAddr() {} struct sockaddr_in *GetNetAddress() { return _address; } socklen_t Len() { return _len; } ~InetAddr() {} private: // net address struct sockaddr_in _address; socklen_t _len; // host address std::string _ip; uint16_t _port; };在上面的地址转换中inet_ntoa内部使用静态全局缓冲区在多线程环境下存在线程安全问题多线程同时调用会覆盖数据。所以需要做适当修改#pragma once #include iostream #include string #include strings.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h // 对客户端进行先描述 class InetAddr { public: InetAddr(const struct sockaddr_in address):_address(address), _len(sizeof(address)) { // _ip inet_ntoa(_address.sin_addr); char ipstr[32]; inet_ntop(AF_INET, (_address.sin_addr), ipstr, sizeof(ipstr)); _ip ipstr; _port ntohs(_address.sin_port); } InetAddr(uint16_t port, const std::string ip 0.0.0.0):_ip(ip), _port(port) { bzero(_address, sizeof(_address)); _address.sin_family AF_INET; _address.sin_port htons(_port); // h-n //_address.sin_addr.s_addr inet_addr(_ip.c_str()); // 1. 字符串ip-4字节IP 2. hton inet_pton(AF_INET, ip.c_str(), (_address.sin_addr)); _len sizeof(_address); } bool operator (const InetAddr addr) { return (this-_ip addr._ip) (this-_port addr._port); } std::string ToString() { return [ _ip : std::to_string(_port) ]; } InetAddr() {} struct sockaddr_in *GetNetAddress() { return _address; } socklen_t Len() { return _len; } ~InetAddr() {} private: // net address struct sockaddr_in _address; socklen_t _len; // host address std::string _ip; uint16_t _port; };Makefile.PHONY:all all:client_tcp server_tcp server_tcp:EchoTcpServerMain.cc g -o $ $^ -stdc17 -lpthread client_tcp:EchoTcpClient.cc g -o $ $^ -stdc17 -static .PHONY:clean clean: rm -f client_tcp server_tcp以下略往期博客中有Cond.hppMutex.hppThread.hppThreadPool.hppLogger.hpp