第一部分非阻塞写Non-blocking Write深度详解1.1 为什么写操作会阻塞在网络编程中当你调用write(sockfd, data, len)时Linux 内核并不是直接把数据发到网络上而是先把数据从用户空间拷贝到内核的 Socket 发送缓冲区Send Buffer然后由内核协议栈慢慢发出去。阻塞场景如果对端接收很慢或者网络拥堵导致发送缓冲区被填满了此时再调用write()默认行为是线程挂起阻塞直到缓冲区腾出空间。非阻塞场景如果 Socket 是非阻塞的write()不会等而是立刻返回-1并设置errno EAGAIN或EWOULDBLOCK意思是“缓冲区满了你稍后再试”。1.2 非阻塞写的核心难点处理“部分写”非阻塞写最麻烦的地方不是“写不进去”而是“写了一半”。比如你想发 10KB 数据write()只成功写了 4KB 就返回了因为缓冲区只剩 4KB 空间。剩下的 6KB 怎么办解决方案为每个 Socket 连接维护一个用户态发送缓冲区比如std::string或std::vectorchar。如果write()没写完把剩下的数据存入这个缓冲区。向 Epoll 注册EPOLLOUT事件监听“可写”。当 Epoll 通知该 Socket 可写时从缓冲区取出剩下的数据继续写。如果全部写完取消注册EPOLLOUT事件否则 Epoll 会一直通知你可写造成 CPU 浪费。第二部分Epoll LT 与 ET 模式详解Epoll 有两种工作模式决定了它何时通知你以及通知你多少次。2.1 LTLevel Triggered水平触发—— 默认模式特点只要条件满足缓冲区有数据可读 / 缓冲区有空可写Epoll 就会不断地通知你直到你处理完。类比就像门铃只要你不把门打开它就会一直响。读事件只要输入缓冲区里有数据每次epoll_wait都会返回这个事件。写事件只要输出缓冲区有空余空间每次epoll_wait都会返回这个事件。优点编程简单不容易出错。缺点事件通知次数多理论上效率比 ET 稍低但在大多数场景下差异不大。2.2 ETEdge Triggered边缘触发—— 高性能模式特点只有在状态发生变化的瞬间才通知你一次。读事件只有当新数据到来的那一刻缓冲区从空变不空才通知一次。如果你这次没把数据读完Epoll 不会再通知你了直到下次有新数据来。写事件只有当发送缓冲区从满变不满的那一刻才通知一次。类比就像烟花只响一次错过了就没了。强制要求使用 ET 模式时必须将文件描述符设置为非阻塞。编程要求读收到通知后必须循环read()直到返回EAGAIN确保把缓冲区读空。写如果有未发完的数据收到通知后必须循环write()直到返回EAGAIN或全部发完。第三部分实战代码我们将实现一个回显服务器Echo Server客户端发什么服务器就发回什么。这是展示非阻塞读写和 Epoll 模式的最佳案例。公共头文件与工具函数首先我们需要一些通用的辅助函数。#includeiostream#includesys/epoll.h#includesys/socket.h#includenetinet/in.h#includearpa/inet.h#includeunistd.h#includefcntl.h#includeerrno.h#includestring.h#includemap#includestring#defineMAX_EVENTS1024#defineBUFFER_SIZE4096#defineSERVER_PORT8888// 1. 设置文件描述符为非阻塞intset_nonblocking(intfd){intflagsfcntl(fd,F_GETFL,0);if(flags-1)return-1;flags|O_NONBLOCK;returnfcntl(fd,F_SETFL,flags);}// 2. 创建并初始化监听 Socketintcreate_listen_socket(){intlisten_fdsocket(AF_INET,SOCK_STREAM,0);if(listen_fd-1){perror(socket failed);return-1;}intopt1;setsockopt(listen_fd,SOL_SOCKET,SO_REUSEADDR,opt,sizeof(opt));structsockaddr_inaddr;bzero(addr,sizeof(addr));addr.sin_familyAF_INET;addr.sin_addr.s_addrhtonl(INADDR_ANY);addr.sin_porthtons(SERVER_PORT);if(bind(listen_fd,(structsockaddr*)addr,sizeof(addr))-1){perror(bind failed);close(listen_fd);return-1;}if(listen(listen_fd,128)-1){perror(listen failed);close(listen_fd);return-1;}set_nonblocking(listen_fd);// 监听 FD 也设为非阻塞returnlisten_fd;}// 3. 定义连接上下文用于保存每个客户端的未发送完的数据structConnContext{intfd;std::string write_buffer;// 待发送数据的缓冲区};示例一Epoll LT 模式水平触发这是最稳健、最容易写对的模式。// ---------------- LT 模式主函数 ----------------voidrun_lt_server(){intlisten_fdcreate_listen_socket();intepoll_fdepoll_create1(0);// 注册监听 FD 到 Epoll (LT 模式默认就是 LT)structepoll_eventevent;event.data.fdlisten_fd;event.eventsEPOLLIN;epoll_ctl(epoll_fd,EPOLL_CTL_ADD,listen_fd,event);std::mapint,ConnContext*contexts;// 管理所有连接的上下文structepoll_eventevents[MAX_EVENTS];std::coutLT Server running on port SERVER_PORT...std::endl;while(true){intnepoll_wait(epoll_fd,events,MAX_EVENTS,-1);for(inti0;in;i){intsockfdevents[i].data.fd;// 1. 处理新连接if(sockfdlisten_fd){while(true){structsockaddr_inclient_addr;socklen_t lensizeof(client_addr);intconn_fdaccept(listen_fd,(structsockaddr*)client_addr,len);if(conn_fd-1){if(errnoEAGAIN||errnoEWOULDBLOCK)break;// 没有新连接了perror(accept failed);break;}std::coutNew client connected: conn_fdstd::endl;set_nonblocking(conn_fd);// 初始化上下文ConnContext*ctxnewConnContext();ctx-fdconn_fd;contexts[conn_fd]ctx;// 注册读事件 (LT 模式)event.data.ptrctx;// 这里用 ptr 携带上下文比 fd 更方便event.eventsEPOLLIN;epoll_ctl(epoll_fd,EPOLL_CTL_ADD,conn_fd,event);}}// 2. 处理普通事件else{ConnContext*ctx(ConnContext*)events[i].data.ptr;// 情况 A可读事件if(events[i].eventsEPOLLIN){charbuf[BUFFER_SIZE];// LT 模式下这里可以不用 while 循环读但建议还是读干净// 因为如果没读完epoll_wait 下次还会通知ssize_t bytes_readread(ctx-fd,buf,sizeof(buf));if(bytes_read0){// 收到数据放入写缓冲区模拟回显逻辑ctx-write_buffer.append(buf,bytes_read);// 关键因为有数据要发我们需要监听 EPOLLOUT// 重新注册事件同时加上 EPOLLOUTevent.data.ptrctx;event.eventsEPOLLIN|EPOLLOUT;epoll_ctl(epoll_fd,EPOLL_CTL_MOD,ctx-fd,event);}elseif(bytes_read0){std::coutClient disconnected: ctx-fdstd::endl;close(ctx-fd);deletectx;contexts.erase(sockfd);}else{if(errno!EAGAIN){perror(read error);close(ctx-fd);deletectx;contexts.erase(sockfd);}}}// 情况 B可写事件if(events[i].eventsEPOLLOUT){if(!ctx-write_buffer.empty()){// 尝试发送数据ssize_t bytes_writtenwrite(ctx-fd,ctx-write_buffer.c_str(),ctx-write_buffer.size());if(bytes_written0){// 移除已发送的部分ctx-write_buffer.erase(0,bytes_written);}elseif(bytes_written-1errno!EAGAIN){perror(write error);}}// 如果缓冲区空了取消监听 EPOLLOUT避免空转if(ctx-write_buffer.empty()){event.data.ptrctx;event.eventsEPOLLIN;// 只保留读事件epoll_ctl(epoll_fd,EPOLL_CTL_MOD,ctx-fd,event);}}}}}close(listen_fd);}intmain(){run_lt_server();return0;}示例二Epoll ET 模式边缘触发注意看代码中while循环的位置这是 ET 模式的关键。// ---------------- ET 模式主函数 ----------------voidrun_et_server(){intlisten_fdcreate_listen_socket();intepoll_fdepoll_create1(0);// 注册监听 FD 到 Epoll (注意加上 EPOLLET 标志)structepoll_eventevent;event.data.fdlisten_fd;event.eventsEPOLLIN|EPOLLET;// --- 关键ET 模式epoll_ctl(epoll_fd,EPOLL_CTL_ADD,listen_fd,event);std::mapint,ConnContext*contexts;structepoll_eventevents[MAX_EVENTS];std::coutET Server running on port SERVER_PORT...std::endl;while(true){intnepoll_wait(epoll_fd,events,MAX_EVENTS,-1);for(inti0;in;i){intsockfdevents[i].data.fd;// 1. 处理新连接if(sockfdlisten_fd){// ET 模式下accept 也必须用 while 循环// 因为如果同时来 10 个连接ET 只通知一次必须一次性 accept 完while(true){structsockaddr_inclient_addr;socklen_t lensizeof(client_addr);intconn_fdaccept(listen_fd,(structsockaddr*)client_addr,len);if(conn_fd-1){if(errnoEAGAIN||errnoEWOULDBLOCK)break;perror(accept failed);break;}std::coutNew client connected: conn_fdstd::endl;set_nonblocking(conn_fd);ConnContext*ctxnewConnContext();ctx-fdconn_fd;contexts[conn_fd]ctx;// 注册读事件 (ET 模式)event.data.ptrctx;event.eventsEPOLLIN|EPOLLET;// --- ET 模式epoll_ctl(epoll_fd,EPOLL_CTL_ADD,conn_fd,event);}}// 2. 处理普通事件else{ConnContext*ctx(ConnContext*)events[i].data.ptr;boolerrorfalse;// 情况 A可读事件if(events[i].eventsEPOLLIN){// 【ET 核心】必须用 while 循环读直到返回 EAGAIN// 因为只通知这一次不读干净下次就没机会了除非有新数据while(true){charbuf[BUFFER_SIZE];ssize_t bytes_readread(ctx-fd,buf,sizeof(buf));if(bytes_read0){ctx-write_buffer.append(buf,bytes_read);}elseif(bytes_read0){std::coutClient disconnected: ctx-fdstd::endl;errortrue;break;}else{if(errnoEAGAIN||errnoEWOULDBLOCK){break;// 数据读完了}perror(read error);errortrue;break;}}// 如果有数据要发注册 EPOLLOUTif(!ctx-write_buffer.empty()!error){event.data.ptrctx;// 注意ET 模式下EPOLLOUT 通常不需要一直注册// 只有当写缓冲区满了没写完时我们才需要注册它// 这里为了简化演示我们先直接尝试写写不完再注册// 但标准做法是先尝试 write如果返回 EAGAIN再注册 EPOLLOUTevent.eventsEPOLLIN|EPOLLOUT|EPOLLET;epoll_ctl(epoll_fd,EPOLL_CTL_MOD,ctx-fd,event);}}// 情况 B可写事件if(!error(events[i].eventsEPOLLOUT)){// 【ET 核心】必须用 while 循环写直到把缓冲区清空或返回 EAGAINwhile(!ctx-write_buffer.empty()){ssize_t bytes_writtenwrite(ctx-fd,ctx-write_buffer.c_str(),ctx-write_buffer.size());if(bytes_written0){ctx-write_buffer.erase(0,bytes_written);}elseif(bytes_written-1){if(errnoEAGAIN||errnoEWOULDBLOCK){// 缓冲区又满了这次写不完了等下次 EPOLLOUT 通知break;}perror(write error);errortrue;break;}}// 如果写完了取消 EPOLLOUT (非常重要节省资源)if(ctx-write_buffer.empty()!error){event.data.ptrctx;event.eventsEPOLLIN|EPOLLET;epoll_ctl(epoll_fd,EPOLL_CTL_MOD,ctx-fd,event);}}if(error){close(ctx-fd);deletectx;contexts.erase(sockfd);}}}}close(listen_fd);}intmain(){run_et_server();return0;}总结与对比特性LT (水平触发)ET (边缘触发)通知机制只要数据没处理完就一直通知只在状态变化时通知一次编程复杂度简单不易出错复杂必须配合循环读写性能稍低 (通知次数多)理论更高 (减少系统调用)FD 要求阻塞/非阻塞均可必须非阻塞适用场景通用业务逻辑复杂高性能连接数极多 (如 C10K)测试方法编译上述任一代码后使用telnet或nc(netcat) 进行测试nclocalhost8888
非阻塞写的完整逻辑
发布时间:2026/6/15 23:52:03
第一部分非阻塞写Non-blocking Write深度详解1.1 为什么写操作会阻塞在网络编程中当你调用write(sockfd, data, len)时Linux 内核并不是直接把数据发到网络上而是先把数据从用户空间拷贝到内核的 Socket 发送缓冲区Send Buffer然后由内核协议栈慢慢发出去。阻塞场景如果对端接收很慢或者网络拥堵导致发送缓冲区被填满了此时再调用write()默认行为是线程挂起阻塞直到缓冲区腾出空间。非阻塞场景如果 Socket 是非阻塞的write()不会等而是立刻返回-1并设置errno EAGAIN或EWOULDBLOCK意思是“缓冲区满了你稍后再试”。1.2 非阻塞写的核心难点处理“部分写”非阻塞写最麻烦的地方不是“写不进去”而是“写了一半”。比如你想发 10KB 数据write()只成功写了 4KB 就返回了因为缓冲区只剩 4KB 空间。剩下的 6KB 怎么办解决方案为每个 Socket 连接维护一个用户态发送缓冲区比如std::string或std::vectorchar。如果write()没写完把剩下的数据存入这个缓冲区。向 Epoll 注册EPOLLOUT事件监听“可写”。当 Epoll 通知该 Socket 可写时从缓冲区取出剩下的数据继续写。如果全部写完取消注册EPOLLOUT事件否则 Epoll 会一直通知你可写造成 CPU 浪费。第二部分Epoll LT 与 ET 模式详解Epoll 有两种工作模式决定了它何时通知你以及通知你多少次。2.1 LTLevel Triggered水平触发—— 默认模式特点只要条件满足缓冲区有数据可读 / 缓冲区有空可写Epoll 就会不断地通知你直到你处理完。类比就像门铃只要你不把门打开它就会一直响。读事件只要输入缓冲区里有数据每次epoll_wait都会返回这个事件。写事件只要输出缓冲区有空余空间每次epoll_wait都会返回这个事件。优点编程简单不容易出错。缺点事件通知次数多理论上效率比 ET 稍低但在大多数场景下差异不大。2.2 ETEdge Triggered边缘触发—— 高性能模式特点只有在状态发生变化的瞬间才通知你一次。读事件只有当新数据到来的那一刻缓冲区从空变不空才通知一次。如果你这次没把数据读完Epoll 不会再通知你了直到下次有新数据来。写事件只有当发送缓冲区从满变不满的那一刻才通知一次。类比就像烟花只响一次错过了就没了。强制要求使用 ET 模式时必须将文件描述符设置为非阻塞。编程要求读收到通知后必须循环read()直到返回EAGAIN确保把缓冲区读空。写如果有未发完的数据收到通知后必须循环write()直到返回EAGAIN或全部发完。第三部分实战代码我们将实现一个回显服务器Echo Server客户端发什么服务器就发回什么。这是展示非阻塞读写和 Epoll 模式的最佳案例。公共头文件与工具函数首先我们需要一些通用的辅助函数。#includeiostream#includesys/epoll.h#includesys/socket.h#includenetinet/in.h#includearpa/inet.h#includeunistd.h#includefcntl.h#includeerrno.h#includestring.h#includemap#includestring#defineMAX_EVENTS1024#defineBUFFER_SIZE4096#defineSERVER_PORT8888// 1. 设置文件描述符为非阻塞intset_nonblocking(intfd){intflagsfcntl(fd,F_GETFL,0);if(flags-1)return-1;flags|O_NONBLOCK;returnfcntl(fd,F_SETFL,flags);}// 2. 创建并初始化监听 Socketintcreate_listen_socket(){intlisten_fdsocket(AF_INET,SOCK_STREAM,0);if(listen_fd-1){perror(socket failed);return-1;}intopt1;setsockopt(listen_fd,SOL_SOCKET,SO_REUSEADDR,opt,sizeof(opt));structsockaddr_inaddr;bzero(addr,sizeof(addr));addr.sin_familyAF_INET;addr.sin_addr.s_addrhtonl(INADDR_ANY);addr.sin_porthtons(SERVER_PORT);if(bind(listen_fd,(structsockaddr*)addr,sizeof(addr))-1){perror(bind failed);close(listen_fd);return-1;}if(listen(listen_fd,128)-1){perror(listen failed);close(listen_fd);return-1;}set_nonblocking(listen_fd);// 监听 FD 也设为非阻塞returnlisten_fd;}// 3. 定义连接上下文用于保存每个客户端的未发送完的数据structConnContext{intfd;std::string write_buffer;// 待发送数据的缓冲区};示例一Epoll LT 模式水平触发这是最稳健、最容易写对的模式。// ---------------- LT 模式主函数 ----------------voidrun_lt_server(){intlisten_fdcreate_listen_socket();intepoll_fdepoll_create1(0);// 注册监听 FD 到 Epoll (LT 模式默认就是 LT)structepoll_eventevent;event.data.fdlisten_fd;event.eventsEPOLLIN;epoll_ctl(epoll_fd,EPOLL_CTL_ADD,listen_fd,event);std::mapint,ConnContext*contexts;// 管理所有连接的上下文structepoll_eventevents[MAX_EVENTS];std::coutLT Server running on port SERVER_PORT...std::endl;while(true){intnepoll_wait(epoll_fd,events,MAX_EVENTS,-1);for(inti0;in;i){intsockfdevents[i].data.fd;// 1. 处理新连接if(sockfdlisten_fd){while(true){structsockaddr_inclient_addr;socklen_t lensizeof(client_addr);intconn_fdaccept(listen_fd,(structsockaddr*)client_addr,len);if(conn_fd-1){if(errnoEAGAIN||errnoEWOULDBLOCK)break;// 没有新连接了perror(accept failed);break;}std::coutNew client connected: conn_fdstd::endl;set_nonblocking(conn_fd);// 初始化上下文ConnContext*ctxnewConnContext();ctx-fdconn_fd;contexts[conn_fd]ctx;// 注册读事件 (LT 模式)event.data.ptrctx;// 这里用 ptr 携带上下文比 fd 更方便event.eventsEPOLLIN;epoll_ctl(epoll_fd,EPOLL_CTL_ADD,conn_fd,event);}}// 2. 处理普通事件else{ConnContext*ctx(ConnContext*)events[i].data.ptr;// 情况 A可读事件if(events[i].eventsEPOLLIN){charbuf[BUFFER_SIZE];// LT 模式下这里可以不用 while 循环读但建议还是读干净// 因为如果没读完epoll_wait 下次还会通知ssize_t bytes_readread(ctx-fd,buf,sizeof(buf));if(bytes_read0){// 收到数据放入写缓冲区模拟回显逻辑ctx-write_buffer.append(buf,bytes_read);// 关键因为有数据要发我们需要监听 EPOLLOUT// 重新注册事件同时加上 EPOLLOUTevent.data.ptrctx;event.eventsEPOLLIN|EPOLLOUT;epoll_ctl(epoll_fd,EPOLL_CTL_MOD,ctx-fd,event);}elseif(bytes_read0){std::coutClient disconnected: ctx-fdstd::endl;close(ctx-fd);deletectx;contexts.erase(sockfd);}else{if(errno!EAGAIN){perror(read error);close(ctx-fd);deletectx;contexts.erase(sockfd);}}}// 情况 B可写事件if(events[i].eventsEPOLLOUT){if(!ctx-write_buffer.empty()){// 尝试发送数据ssize_t bytes_writtenwrite(ctx-fd,ctx-write_buffer.c_str(),ctx-write_buffer.size());if(bytes_written0){// 移除已发送的部分ctx-write_buffer.erase(0,bytes_written);}elseif(bytes_written-1errno!EAGAIN){perror(write error);}}// 如果缓冲区空了取消监听 EPOLLOUT避免空转if(ctx-write_buffer.empty()){event.data.ptrctx;event.eventsEPOLLIN;// 只保留读事件epoll_ctl(epoll_fd,EPOLL_CTL_MOD,ctx-fd,event);}}}}}close(listen_fd);}intmain(){run_lt_server();return0;}示例二Epoll ET 模式边缘触发注意看代码中while循环的位置这是 ET 模式的关键。// ---------------- ET 模式主函数 ----------------voidrun_et_server(){intlisten_fdcreate_listen_socket();intepoll_fdepoll_create1(0);// 注册监听 FD 到 Epoll (注意加上 EPOLLET 标志)structepoll_eventevent;event.data.fdlisten_fd;event.eventsEPOLLIN|EPOLLET;// --- 关键ET 模式epoll_ctl(epoll_fd,EPOLL_CTL_ADD,listen_fd,event);std::mapint,ConnContext*contexts;structepoll_eventevents[MAX_EVENTS];std::coutET Server running on port SERVER_PORT...std::endl;while(true){intnepoll_wait(epoll_fd,events,MAX_EVENTS,-1);for(inti0;in;i){intsockfdevents[i].data.fd;// 1. 处理新连接if(sockfdlisten_fd){// ET 模式下accept 也必须用 while 循环// 因为如果同时来 10 个连接ET 只通知一次必须一次性 accept 完while(true){structsockaddr_inclient_addr;socklen_t lensizeof(client_addr);intconn_fdaccept(listen_fd,(structsockaddr*)client_addr,len);if(conn_fd-1){if(errnoEAGAIN||errnoEWOULDBLOCK)break;perror(accept failed);break;}std::coutNew client connected: conn_fdstd::endl;set_nonblocking(conn_fd);ConnContext*ctxnewConnContext();ctx-fdconn_fd;contexts[conn_fd]ctx;// 注册读事件 (ET 模式)event.data.ptrctx;event.eventsEPOLLIN|EPOLLET;// --- ET 模式epoll_ctl(epoll_fd,EPOLL_CTL_ADD,conn_fd,event);}}// 2. 处理普通事件else{ConnContext*ctx(ConnContext*)events[i].data.ptr;boolerrorfalse;// 情况 A可读事件if(events[i].eventsEPOLLIN){// 【ET 核心】必须用 while 循环读直到返回 EAGAIN// 因为只通知这一次不读干净下次就没机会了除非有新数据while(true){charbuf[BUFFER_SIZE];ssize_t bytes_readread(ctx-fd,buf,sizeof(buf));if(bytes_read0){ctx-write_buffer.append(buf,bytes_read);}elseif(bytes_read0){std::coutClient disconnected: ctx-fdstd::endl;errortrue;break;}else{if(errnoEAGAIN||errnoEWOULDBLOCK){break;// 数据读完了}perror(read error);errortrue;break;}}// 如果有数据要发注册 EPOLLOUTif(!ctx-write_buffer.empty()!error){event.data.ptrctx;// 注意ET 模式下EPOLLOUT 通常不需要一直注册// 只有当写缓冲区满了没写完时我们才需要注册它// 这里为了简化演示我们先直接尝试写写不完再注册// 但标准做法是先尝试 write如果返回 EAGAIN再注册 EPOLLOUTevent.eventsEPOLLIN|EPOLLOUT|EPOLLET;epoll_ctl(epoll_fd,EPOLL_CTL_MOD,ctx-fd,event);}}// 情况 B可写事件if(!error(events[i].eventsEPOLLOUT)){// 【ET 核心】必须用 while 循环写直到把缓冲区清空或返回 EAGAINwhile(!ctx-write_buffer.empty()){ssize_t bytes_writtenwrite(ctx-fd,ctx-write_buffer.c_str(),ctx-write_buffer.size());if(bytes_written0){ctx-write_buffer.erase(0,bytes_written);}elseif(bytes_written-1){if(errnoEAGAIN||errnoEWOULDBLOCK){// 缓冲区又满了这次写不完了等下次 EPOLLOUT 通知break;}perror(write error);errortrue;break;}}// 如果写完了取消 EPOLLOUT (非常重要节省资源)if(ctx-write_buffer.empty()!error){event.data.ptrctx;event.eventsEPOLLIN|EPOLLET;epoll_ctl(epoll_fd,EPOLL_CTL_MOD,ctx-fd,event);}}if(error){close(ctx-fd);deletectx;contexts.erase(sockfd);}}}}close(listen_fd);}intmain(){run_et_server();return0;}总结与对比特性LT (水平触发)ET (边缘触发)通知机制只要数据没处理完就一直通知只在状态变化时通知一次编程复杂度简单不易出错复杂必须配合循环读写性能稍低 (通知次数多)理论更高 (减少系统调用)FD 要求阻塞/非阻塞均可必须非阻塞适用场景通用业务逻辑复杂高性能连接数极多 (如 C10K)测试方法编译上述任一代码后使用telnet或nc(netcat) 进行测试nclocalhost8888