套接字编程这东西,刚入门的时候总觉得是很高深的技术,各种结构体、各种函数、各种网络术语堆在一起,能把人绕晕。但真正做过几个项目之后回头看,你会发现核心的东西其实就是那么几套固定玩法,TCP一条线,UDP一条线,加并发处理,加异常排查,基本就能覆盖绝大多数日常需求了。这篇东西我打算从一个实操者的角度,把Linux下套接字编程里那些我认为最有价值、也最容易踩坑的部分摊开来讲。不会只贴概念,而是直接带你走一遍从建连到收发、从单进程到高并发的完整链路,每一步都说明白为什么这么做,以及我第一次做的时候在哪里翻过车。
这套能力解决什么问题呢?说白了,它就是让进程之间跨机器通信的底层骨架。你在做的监控上报、日志采集、远程控制、微服务调用、私有协议网关,无论上层多花哨,最终大概率都要落到socket上面来。适合的人群也很明确:准备系统性上手Linux网络编程的C/C++开发者、刚接手网络模块需要快速补课的运维和嵌入式工程师,以及那些在别的语言里调过网络接口、但想搞明白底层到底发生了什么的人。
我一直有个观点,网络编程跟普通业务编程最大的区别在于,你要面对的不只是自己的代码,还有一个不受你控制的网络环境。数据会丢、会重排、会对端突然消失,这些不确定性才是真正的难点。因此这篇文章也花了不少篇幅在错误处理、常见坑和排查手段上,把实战中容易翻车的点尽量都摆出来,希望能帮你少走几趟弯路。
1. 动手之前,先把这几个核心概念吃透
写代码最容易犯的毛病就是手上写着socket,脑子里却没想清楚数据是怎么流过去的。这一步看起来基础,但决定了后面所有调试思路对不对。
1.1 网络模型和数据的流动路径
很多人一提到网络编程就背OSI七层模型,但实际上做Linux套接字编程,你真正需要关心的其实是传输层和以下那几层之间的分工。你的进程通过socket创建一条通往内核协议栈的通道,调用send/write把数据交给内核,内核再按TCP或UDP协议把它们切分、加包头、排队、发到网卡。对端接收到之后,内核根据四元组(源IP、源端口、目标IP、目标端口)把数据送进对应socket的接收缓冲区,你的进程再用recv/read取出来。
理解这个流动过程有什么实际价值呢?我举一个典型场景:你发现客户端发出的数据服务器一直收不到。初级选手会怀疑是recv写错了,老手第一反应是先看数据到底有没有进到内核缓冲区。如果服务端用tcpdump能抓到包,那问题就不在内核和网络,而在你的进程有没有及时从缓冲区拿数据。这个判断在排查的时候能帮你至少节省半天时间。
所以,socket编程的本质就是操作两块缓冲区和几条队列,你需要把这层意识时刻放在脑子里。
1.2 三种套接字类型,别急着选
socket()的第二个参数决定了套接字的类型,最常见的是这三个:
- SOCK_STREAM:面向连接的流式套接字,对应TCP,数据无边界、有序、可靠。
- SOCK_DGRAM:数据报套接字,对应UDP,数据有边界、无连接、不保证到达。
- SOCK_RAW:原始套接字,可以让你直接操作IP层甚至更底层的数据,一般用于抓包工具、组播处理或者某些特殊协议实现。
对绝大多数业务系统来说,根本不需要碰RAW。但有一个点常被忽略:很多人误以为SOCK_STREAM就是“TCP”,SOCK_DGRAM就是“UDP”。严格说,这只是套接字类型,协议是在第三个参数里指定的。用SOCK_STREAM加IPPROTO_TCP才是TCP,用SOCK_DGRAM加IPPROTO_UDP才是UDP。虽然你用0作为第三个参数时,系统会自动匹配默认协议,但我在工程习惯上还是会显式写清楚,不为别的,就为了代码可读性和以后review的时候少让人猜。
1.3 字节序问题,新手在这里吃过太多亏
这是一个特别容易出问题的地方,而且它一旦出错,数据看起来是通了的,但内容全是乱的。
我见过一个实际案例,A同学写了一个服务端,接收客户端发来的设备消息,解析长度字段时读出来总是几千几万这样的诡异数字。后来排查了半天,才发现客户端采用的x86小端序,直接把结构体内存指针扔给send()发送,服务端收到的多字节整数按网络字节序解析,结果自然全乱套。解决这个问题的唯一正确姿势就是统一用htons/htonl/ntohs/ntohl这组接口做转换,无论什么平台,一律先转成网络字节序再进传输层,读取时再转回主机字节序。
还有一点新手容易遗漏:不只是整数字段,凡是大于一个字节的数值型字段,在自定义协议里都必须约定字节序。字符串和字节数组不受影响,但长度、类型、校验值这些几乎都需要转换。
1.4 地址结构体:初始化不干净,bind必然出幺蛾子
写TCP服务端第一步就是建socket、初始化地址、bind。很多网上教程喜欢这样写:
struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(8080); addr.sin_addr.s_addr = htonl(INADDR_ANY);这种情况大多数时候能跑,但我强烈建议你在定义之后立刻memset,或者说用结构体初始化器的方式清零。原因很简单,sockaddr_in结构体里除了你看到的这几个字段,还有隐式填充的pad字段和sin_zero。如果你分配了一个栈上变量不初始化,里面是随机的栈残留数据,bind的时候内核可能因为那些垃圾数据行为异常。有些内核版本会直接返回EADDRINUSE,有些怪异的场景下甚至会让bind成功但行为诡异,排查起来特别恶心。
我的习惯是:
struct sockaddr_in addr = { .sin_family = AF_INET, .sin_port = htons(SERVER_PORT), .sin_addr.s_addr = htonl(INADDR_ANY) };这样即使后面加了字段,编译器也会自动清零剩下的部分,比memset更加稳妥,代码也更简洁。
2. TCP服务端:从零开始,一步步跑通
TCP是网络编程里绝对的主角。这里我想带你完整走一遍服务端的生命周期,每一步都说明参数的意义和容易忽略的细节。
2.1 socket的创建和常见的两个坑
int listen_fd = socket(AF_INET, SOCK_STREAM, 0); if (listen_fd < 0) { perror("socket"); exit(EXIT_FAILURE); }第一个坑是缺省情况下socket默认是阻塞模式。在阻塞模式下,accept、read、write在某些时机可能长时间卡住。尤其是初学者,写完一个单线程服务端之后发现客户端一连接,程序就卡在accept等下一次连接,这其实是符合设计的,但如果你想要“能同时服务多个连接却只写一个循环”,就必须引入多进程、多线程或IO多路复用,不然进程会被第一个连接的阻塞读永久挂住。
第二个坑是文件描述符上限。一个socket成功创建后返回的是一个普通int类型的文件描述符。系统默认的进程fd上限通常是1024,也就是同一个进程最多同时打开约1021个连接。很多初学者压测到第一千左右把服务端压挂,第一反应是代码出问题,实际上往往是ulimit限制。处理高并发前一定要先把资源限制调上去,或者用systemd的LimitNOFILE统一管理。
2.2 bind和listen的关键参数
bind这一步是把你的服务端绑定到一个固定地址和端口上。端口选择有几个原则:
- 小于1024的端口需要root权限,除非确认需要,否则不建议让业务进程直接跑1024以下,安全风险太大。
- bind到0.0.0.0(INADDR_ANY)会监听本机所有网卡上对应的端口,适合通用服务。
- bind到127.0.0.1则只服务于本机回环访问,外部机器无法连接,适合本地调试或者不希望暴露的服务。
listen函数的第二个参数backlog,经受过不少人误解。它不是最大并发连接数,而是内核维护的已完成连接队列(accept队列)的最大长度。也就是说,当你的进程来不及调用accept时,连接请求最多在队列里排这么多。如果队列满了,新的连接请求就会被内核拒绝,客户端会收到connection refused。这里我个人的经验是,在Linux 2.6以上的内核里,backlog一般填128或者256就够了,不必往死里加。真正决定并发上限的是后续采用哪种IO模型,而不是这个队列长度。
2.3 accept到的连接,本质上仍是一个socket
有一点很重要,accept()返回的并不是同一个fd,而是内核根据已完成三次握手新建的一个连接socket。连接socket和服务端监听socket是两个独立的文件描述符,各有各的缓冲区。很多人一开始不理解为什么accept之后还需要继续维护listen_fd,原因就在这里。listen_fd要一直留在手里,用它继续接收新的连接。
我用最简单的伪代码描述一下TCP服务端的常态循环:
while (1) { int conn_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &addr_len); if (conn_fd < 0) { // EINTR / EAGAIN 要特殊处理 continue; } // 处理这个连接,注意绝不能在单线程里同步地直接和客户端收发 }如果你在这里同步收发,这个程序就废了,因为服务端会被第一个连接占满。业界通用的做法是,accept拿到conn_fd之后,立刻把处理逻辑交给子进程、子线程或者放进epoll事件循环,再回到循环继续accept。这一点我后面专门展开讲。
2.4 收发数据的正确姿势
收数据看着简单,就一个read/recv,但坑非常多。第一个大前提:TCP是流协议,没有消息边界。你send了100个字节,对端recv一次可能只收到50个,也可能一次收到200个(因为前一条消息还没读完就又收到了新的)。所以接收方绝对不能依赖“读一次就等于一条完整消息”这种假设。
正确做法是维护一个接收缓冲区,把recv到的数据全部追加进去,然后从里面按照应用层协议去解析完整的消息。消息边界怎么定义?常见做法有三种:
- 固定长度:每条消息长度固定,读够长度就解析。
- 长度前缀:每条消息开头放4字节长度字段,先读长度,再按长度读消息体。
- 分隔符:比如HTTP的\r\n\r\n,或者文本协议里的换行符。
这三种方案里,固定长度最简单但浪费带宽,长度前缀最通用,分隔符适合文本协议。我个人在项目里用得最多的是“1个字节消息类型+4字节长度+变长消息体”的组合,灵活性和可扩展性都很好。
发送数据相对简单,但send并不保证完全发送。send返回的值可能小于你传入的长度,常见于发送缓冲区满或信号中断。所以完整的发送应该写成循环:
ssize_t send_all(int fd, const char *buf, size_t len) { size_t sent = 0; while (sent < len) { ssize_t n = send(fd, buf + sent, len - sent, MSG_NOSIGNAL); if (n < 0) { if (errno == EINTR) continue; return -1; } sent += n; } return sent; }这里有一个很多人不知道的技巧:用MSG_NOSIGNAL标志,避免对端关闭连接时,send触发SIGPIPE信号直接把你的进程干掉。默认情况下,向一个已经关闭的连接继续写数据会导致内核发送SIGPIPE给进程,进程的默认行为就是退出。很多服务端程序莫名其妙崩溃,日志却什么都没留下,就是因为它死在了SIGPIPE信号上。如果你不想每个send都加MSG_NOSIGNAL,也可以在程序开头用signal(SIGPIPE, SIG_IGN)全局屏蔽,二选一,但千万别什么都不管。
3. UDP编程:比TCP简单,但陷阱也不少
很多人觉得UDP随便一发一收就完了,这个心态会害死你。UDP编程的代码量确实比TCP少很多,但不代表它可以被轻视。
3.1 UDP服务端的基本模型
UDP服务端的socket创建类型换一下,bind流程完全一样,但之后就完全没有listen/accept了。它只负责bind到这个端口,然后直接recvfrom/write逻辑:
int udp_fd = socket(AF_INET, SOCK_DGRAM, 0); // bind 到端口 char buffer[2048]; struct sockaddr_in peer_addr; socklen_t addr_len = sizeof(peer_addr); ssize_t n = recvfrom(udp_fd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer_addr, &addr_len); if (n > 0) { // 注意:recvfrom 填满了 peer_addr sendto(udp_fd, buffer, n, 0, (struct sockaddr *)&peer_addr, addr_len); }看到区别了吗?UDP的连接信息不在socket内部,而是每次收发都要显式带上对端地址。所以即使是一个UDP“连接”,你也要在应用层维护对端地址,经常是一个表结构,保存每个客户端最近一次的来源地址。
3.2 UDP的“零连接”不等于零维护
有几个点要强调:
第一,recvfrom里的buffer大小非常重要。UDP数据报有边界,如果buffer小于到达的数据报长度,内核会直接截断,剩下的数据直接丢弃。我遇到过真实事故:某个采集服务的buffer设了1024,客户端发了1500字节的报文,服务端每次只解析出前1024字节,后面的字段全是空的,而且因为UDP无连接、无重传,这个错误很难被发现。建议UDP接收缓冲区尽量设大,至少要覆盖你业务里最大消息体的理论值。
第二,UDP虽然没有三次握手,但你依然要注意端口复用问题。多个进程同时bind同一个UDP端口时,Linux较新版本支持SO_REUSEPORT做负载均衡,但老版本上第二个bind会直接失败。这种问题在不熟悉socket选项的工程师那里,往往排查很久。
第三点是性能问题。不要以为UDP就不会丢包。在内核接收缓冲区满的时候,UDP会静默丢包。如果你的服务要承载高吞吐,记得用setsockopt增大SO_RCVBUF,并且一定要检查setsockopt的返回值。很多情况下,内核会悄悄把请求值调小,你不校验返回值就以为设置成功了,实际上缓冲区还是默认值,压力一大就开始丢。
3.3 什么时候选UDP,什么时候坚持TCP
这是我被问得最多的问题之一。我的判断标准一直是:能不能容忍丢包和数据乱序。
能容忍,且对实时性要求极高,比如实时视频传输、游戏状态同步,用UDP,自己在上层做丢包重传或者冗余编码。不能容忍,或者处理不了乱序,就老老实实TCP。千万不要觉得“UDP快所以用它准没错”,在跨公网传输业务数据时,UDP的丢包重传全部要自己实现,工程的复杂度往往比TCP高一个数量级。
4. 并发能力:从单进程到epoll
一个只能处理一个连接的服务端基本没有实用价值。并发方案选型这件事,决定了你的服务能撑到多少连接。我按从简单到复杂把主流方案过一遍,顺便把各自适合的场景说清楚。
4.1 fork/clone:写起来最直接,但代价很高
传统做法是accept之后fork一个子进程或者pthread创建一个线程去处理。这个模型的好处是简单直观,每个连接一个独立的上下文,阻塞读也不会互相影响。坏处是每来一个连接就要创建一次进程/线程,创建销毁的开销非常大,而且系统能创建的进程数和线程数都有限。
我做压测时的感受是,用线程池代替临时创建会有明显改善。也就是说,启动时创建一批工作线程,accept来的连接丢进任务队列,工作线程去处理。这个方案在连接数几百的时候完全够用,代码也不难写,是一个性价比很高的中间方案。但连接数一旦破万,线程切换成本和内存占用就压不住了,这时候就得换思路。
4.2 select和poll:适合理解,不适合高并发
select和poll的核心思路是把一堆fd交给内核,让它告诉你哪些可读哪些可写。你有多个连接同时等数据,但不需要为每个连接起一个线程。
select最大的问题在于,它每次调用都要把整个fd集合从用户态拷贝到内核态,而且fd数量上限由FD_SETSIZE决定,默认只有1024。即使你修改宏重新编译,效率还是很低,因为内核是线性扫描所有fd的。
poll略微改进了数量限制,但本质上还是扫描法。在连接数几千以内、业务逻辑不复杂的小工具里,poll完全够用。但如果你想写一个能支撑上万连接的高并发服务,这两者都不是合适的终点站,真正能打的还是epoll。
4.3 epoll:Linux高并发的通用答案
epoll和select/poll最大的区别在于两点:一是内核维护了事件就绪链表,你每次只要取就绪事件就行了,不需要扫描全部fd;二是通过epoll_ctl注册监听事件,注册一次长期有效,不需要像select那样每次重新构造集合。
最基础的用法是水平触发模式:
struct epoll_event ev; int epfd = epoll_create1(0); ev.events = EPOLLIN; ev.data.fd = listen_fd; epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev); while (1) { struct epoll_event events[MAX_EVENTS]; int n = epoll_wait(epfd, events, MAX_EVENTS, -1); for (int i = 0; i < n; i++) { if (events[i].data.fd == listen_fd) { int conn_fd = accept(listen_fd, NULL, NULL); ev.events = EPOLLIN | EPOLLRDHUP; ev.data.fd = conn_fd; epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev); } else { handle_read(events[i].data.fd); } } }这里我强烈建议一个细节:注册EPOLLRDHUP。这个标志表示对端关闭连接时立即通知你,你可以马上关闭本地fd,避免read返回0才察觉,也为处理半关闭场景提供更及时的反馈。
另一个重要知识点是EPOLLET边缘触发。边缘触发只在状态变化时通知一次,配合非阻塞fd使用效率高,但它对代码要求很高。你必须一次把缓冲区里的数据全部读完,直到返回EAGAIN,否则剩余数据不会再次触发事件,会造成数据卡住。我个人的工程偏好是,能不用边缘触发就不用,大部分场景下水平触发配合合理的read循环已经把内核打满,没必要拿边缘触发来炫技,省那点事件通知开销,很可能被一次漏读带来的bug全部赔回去。
4.4 非阻塞IO与EAGAIN
任何高并发方案都绕不开非阻塞IO。当你把fd设为非阻塞,read/recv时如果没有数据可读,函数会立即返回-1,并且errno被置为EAGAIN或EWOULDBLOCK。这不是错误,而是一种正常情况。
int flags = fcntl(fd, F_GETFL, 0); fcntl(fd, F_SETFL, flags | O_NONBLOCK);配合epoll读数据的标准姿势是循环读到EAGAIN为止。注意在边缘触发下这一步是必须的,在水平触发下其实你不读出来,后续事件还是会触发。但统一的习惯是:只要可读就继续读,直到EAGAIN,这样代码在两种触发模式下都不会出问题。
5. 调试和排错:我在生产环境里用过的三板斧
代码写完只是开始,真正让一个网络程序稳定的关键在调试能力。我强烈建议每个做网络开发的工程师熟练使用下面三个工具:strace、tcpdump、gdb。它们解决的是不同层面的问题。
5.1 strace:看系统调用是否和你想象的一致
当你的服务端表现怪异,比如客户端明明连上了但服务端没反应,第一件事就是在服务端进程上strace:
strace -f -o /tmp/srv_trace.log -p <pid>然后看服务端到底有没有收到accept、有没有read调用、read返回了什么。我曾经遇到一个场景,服务端进程卡在一个地方不动,客户端日志显示已经发送成功。我strace上去一看,进程压根没有调用recv,一直在执行某个定时任务的逻辑。问题根本不在网络层,而是业务逻辑里的调度把读操作饿死了。这种问题不靠strace基本不可能定位。
5.2 tcpdump和Wireshark:确认网络层的事实
服务端和客户端处于同一台机器时,很多人习惯只看程序日志,但程序日志会骗人。收发缓冲区、内核协议栈这些环节不会记录在你的应用日志里。用tcpdump抓包,看到的就是网卡上实际流动的数据包。
tcpdump -i any -nn -s 0 port 8080 -w /tmp/cap.pcap抓完包后用Wireshark打开,分析三次握手、传输序列、重传、断开过程,几秒钟能看清真相。我在排查“客户端一会儿能连一会儿Connection refused”这类问题时,抓包看SYN包是否有回包,能快速判断是不是backlog队列满了,或者是不是iptables、SELinux把包拦了。
这里补充一个容易被忽略的点:本机两个进程通信走的是回环接口lo,抓包要用-i lo。不要默认抓eth0,不然什么都看不到。
5.3 排查经典故障的经验表
| 现象 | 大概率原因 | 建议排查动作 |
|---|---|---|
| bind报Address already in use | 端口被占用,或者处于TIME_WAIT状态 | 用netstat -tunlp定位进程;需要快速重启时设置SO_REUSEADDR |
| connect报Connection timed out | 对端IP不通,或防火墙丢弃SYN | ping、traceroute、telnet试端口,抓包看SYN |
| connect报Connection refused | 对端没有服务监听,或防火墙RST | 确认端口监听状态,抓包确认RST来源 |
| send成功但对端收不到 | 数据滞留在本端内核缓冲区,或对端应用没读取 | tcpdump抓对端入口,strace对端进程 |
| 程序莫名退出但无日志 | SIGPIPE信号 | 检查send代码是否用了MSG_NOSIGNAL,或全局屏蔽SIGPIPE |
| 收数据偶尔粘包/拆包 | TCP流边界误判 | 检查接收方是否按协议字段解析消息边界 |
5.4 你以为的并发问题,其实是资源问题
再补一个实战观察。很多自测时跑得好好的服务,放到生产环境一压测就崩。我用过的排查顺序是:
先看文件描述符数量:ulimit -n以及/proc/<pid>/fd里的实际数量。再看内存:每个TCP连接默认收发缓冲区加起来可能有大几十KB,一万个连接就是几百MB到上GB的占用,内存不足时会触发OOM,进程被内核干掉。再看线程数量:如果用的线程池模型,多的线程数会让调度器不堪重负。最后才是代码逻辑问题。
这个顺序很重要。我见过不少团队在一开始就往代码逻辑方向查,最后绕了一大圈才发现只是文件描述符上限不够。
6. 工程落地中的代码习惯和注意事项
这部分我想聊聊那些在课本里不怎么讲,但实际维护代码库时频繁遇到的问题。
6.1 TIME_WAIT和端口复用
四次挥手后,主动关闭连接的一方会进入TIME_WAIT状态,持续大约两分钟。对于短连接类型的服务端,如果你每次处理完都主动关连接,你会发现系统里堆满了TIME_WAIT状态的连接。这是TCP协议为了处理旧数据包残留而设计的,改不了。但你可以做三件事:
- 让服务端不做主动关闭方,也就是尽量由客户端先关连接。
- 设置SO_LINGER来快速关闭,但代价是丢弃该连接的未发送数据,非必要不推荐。
- 设置SO_REUSEADDR,让服务端重启后可以立即bind同一个端口,而不是等TIME_WAIT全部消失。
我一般把SO_REUSEADDR当作服务端socket的标配参数,不管当前有没有遇到问题,都加上,反正对它没有副作用。
6.2 日志和监控:网络程序绝对不可省的体力活
你会发现,排查网络问题七成时间都花在“看当时发生了什么”上。如果没有日志,基本上两眼一抹黑。我在工程里有一个强制性要求:所有关键的连接事件必须打日志,包括客户端连接建立、断开、超时、异常关闭,以及消息长度非法这类协议错误。
日志不用太花哨,但至少要有时间和两个关键标识(对端IP和端口)。出了线上问题,第一件事就是翻这两个标识,用它把一条连接从建立到断开的全生命周期串起来。说句难听的,很多团队的线上问题,不是技术没到位,是连“搞清楚什么时候、哪个客户端、干了什么”这件事都做不到。
6.3 协议设计应该遵循的原则
套接字编程里的很多混乱,根子都在协议设计上。我近几年最大的体会是:协议设计时多花一小时,后面能省一周。
我的建议有这么几条:
- 消息头一定要有魔数或协议版本,防止对端接入别的系统后你还在用老格式解析。
- 长度字段要包含消息体长度,但不要把消息头本身的长度也塞进去,否则两头一改就混乱。
- 所有多字节数字统一用网络字节序,不要出现“这端改了那端忘了”的情况。
- 尽量为每类消息定义类型字段,方便对端路由和后续扩展。
你要是接手过几个历史项目就会发现,真正难维护的不是协议复杂,而是协议没有明确的规范,每个人按照自己理解实现了不同的格式。规范的约束是必须用代码落实下去的,靠口头约定一定会出问题。
7. 最后再给你一个实测过的建议
这个内容写到这里,基础链路大家应该已经清楚了。最后我分享一个我自己经过多次实践后固定下来的工程套路,就是“先用最朴素的方式把整条链路跑通,再谈优化扩展”。
很多初学者容易一上来就写epoll,结果代码里一堆事件绑定、缓冲区管理,出错时连基本的三次握手通没通都不知道。我更推荐刚起步时先写一个阻塞模式的单进程服务端,配合telnet或nc手动测通收发包,确认协议、字节序、消息边界都没问题后,再切到epoll模型。毕竟网络编程的复杂度主要来自网络本身,如果底层链路都不稳定,你很难判断并发模型写得好不好。
另外,强烈建议在开发阶段就开好编译器的警告选项,比如gcc的-Wall -Wextra。网络编程的代码里,很多bug来自类型不匹配、未初始化变量和忽略返回值。编译器的警告能帮你拦截相当一大部分。运行阶段的监控不要等到上线后再补,起步就把连接数、收发字节数、错误计数打进指标系统。等你真的遇到线上故障时,这些数字就是你定位问题的唯一线索。
网络编程这门技术,入门不难,难的是把你手上的每一行代码都放在真实的网络环境中去推演。写完一个函数,多问自己一句:如果对端半途断电怎么办?如果数据只来了一半怎么办?如果同时来了一千个连接怎么办?把这几个问题想明白了,你的程序就不会太差。