Linux网络编程入门:从Socket与TCP/IP分层到三次握手实战
第一次在Linux上写网络程序很多人第一反应是翻man文档查socket的用法我也不例外。但真正让我从“会调用API”变成“理解网络”的反而不是那几个函数签名而是把脑子里那台“独立主机”的模型拆掉换成一张协议协作的地图。这篇是Linux网络编程系列的第一篇目标是带你完成这个思维转换从单机进程通信的视角切换到多主机协议协作的视角把IP、端口、Socket、TCP/IP分层这些概念串成一条线最后亲手跑通一个最小的TCP程序并用tcpdump亲眼看到三次握手。不管是还没写过网络代码的新手还是写过一点但总被奇怪问题卡住的人这篇都适用。1. 先从单机思维切换到网络思维1.1 单机世界的通信方式以及它们的边界Linux里进程与进程之间怎么通信管道、消息队列、共享内存、信号、本地套接字这些都是经典机制。管道就像一条水管一端写进去另一端读出来共享内存更像是把一张纸贴在墙上多个进程都能看。这些机制用起来都很顺手但有一个共同的边界条件它们都只能在同一台主机上工作。为什么因为管道、消息队列、共享内存本质上依赖内核中的公共资源比如一个文件描述符、一段共享内存区域。两个进程哪怕隔着一堵墙只要在同一台Linux系统上就能通过内核搭桥。但如果进程A在笔记本上进程B在千里之外的服务器上内核不是同一个内存不是同一片管道和共享内存立刻失效。网络编程要解决的就是这个跨主机的问题。要让两个运行在不同机器上的进程像在同一台机器上一样交换数据我们必须设计一套机制怎么找到对方寻址、怎么把数据安全送到传输控制、中间经过哪些设备路由、数据坏了怎么办差错检测。这一整套机制叠加起来就是我们常说的“协议栈”。所以学网络编程本质上学的不是某个API而是这套跨主机通信的规则体系。1.2 通信的前提是“约定”协议就是这套约定想象一下两个人打电话。拨号之前双方其实默认遵守了一整套流程主叫方先拨号被叫方听到铃声后接听然后一方说“喂”另一方回应双方确认能听到声音后才开始讲正事。这套流程看起来稀松平常但它保证了通信的双方在同一时间、同一频率、同一语言体系下工作。如果没有这套约定一方用手机、一方用对讲机或者一方说中文、一方说德语这个电话根本打不成。网络协议也一样它就是通信双方事先约定好的规则。专业一点说协议包含三要素语法规定数据怎么组织比如字段顺序、格式语义规定每个字段代表什么含义比如某个标志位为1表示数据结束时序规定事件发生的顺序比如先建立连接再传数据传完数据再断开。举个例子一个最简单的HTTP GET请求报文长这样GET /index.html HTTP/1.1\r\n Host: www.example.com\r\n \r\n这串文本就是一个应用层协议的具体体现。GET是方法/index.html是路径HTTP/1.1是版本号\r\n是行结束符。服务器看到这串文本就能明白客户端想要什么并按照同样的规则返回响应。你可能会问为什么是GET而不是拿因为协议必须是双方都懂的语言它不依赖人的自然语言而是依赖文档规范。互联网上大量协议的标准定义在RFC文档里任何人都能查阅和实现。所以请记住这句话网络编程的核心是把你对“通信过程的想象”变成双方都能理解的协议。代码只是协议的载体API只是敲门砖。2. 分层模型协议世界的地基2.1 为什么协议非得分层不能一个大包如果从头设计一套跨主机通信方案你会发现要处理的问题实在太多。要寻址要路由要保证数据不丢要处理乱序要流控还要区分不同应用。把所有逻辑塞进一个协议结果就是一个巨型怪物任何一个环节要升级整个协议都得推翻任何一个地方出错排查范围就是海量的。分层是解决这种复杂度的经典手段。你可以把整个通信过程按职责拆成几个相对独立的层每一层只干一件明确的事层与层之间通过标准接口对接。生活中最典型的就是快递系统。商家不关心快递员怎么运输快递员不关心包裹里装的是什么仓库工人只负责分拣和贴单。每一层的职责都足够单纯也可以独立更换——比如商家换了包装盒快递公司完全不需要知道。网络协议栈的设计思路正是这样。通过分层每一层可以独立演化物理层从以太网换成WiFi、从铜线换成光纤应用层的HTTP和SSH完全感知不到变化。排错也因此有了明确方向链路层的问题查MAC地址和交换机网络层的问题查IP和路由传输层的问题查端口和连接状态应用层的问题查报文格式和业务逻辑。2.2 先记TCP/IP四层暂缓OSI七层很多初学者一上来就被“OSI七层模型”劝退。但现实中真正落地的是TCP/IP协议族它的分层方式和OSI并不完全一致。与其死记七层的抽象概念不如先建立TCP/IP四层模型这张图分层核心职责代表协议寻址方式应用层为具体应用提供数据语义HTTP、FTP、SSH、DNS无固定寻址靠协议内部字段传输层端到端的可靠传输区分具体应用TCP、UDP端口号网络层跨网络的主机寻址与路由IP、ICMPIP地址链路层物理网络内的帧传输以太网、WiFi、VLANMAC地址你只需要先记住这个四层模型就够了。应用层处理“数据是什么”传输层处理“数据交给哪个进程”网络层处理“数据去哪台机器”链路层处理“数据怎么在网线上跑”。OSI七层里的会话层、表示层在实际的TCP/IP体系里基本被合并进应用层初学阶段暂时搞不清楚没关系等以后分析具体协议时再回头看也不迟。有个细节值得留意链路层的以太网帧里有一个VLAN标签字段802.1Q用来标记帧属于哪个虚拟局域网。如果你在公司网络里抓包看到vlan 100之类的信息这就是链路层在干活跟我们常说的IP地址不在同一个层面。2.3 从发送到接收数据包的封装与解封装分层模型在数据流动中体现得最直观。假设你在浏览器里敲下http://www.example.com并回车数据是这样从上往下走的应用层把HTTP请求报文交给传输层传输层的TCP在报文前面加上一个TCP头这个头里最关键的是源端口和目的端口比如源端口随机分配一个49152目的端口是80。接着网络层的IP在TCP头前面再加一个IP头里面写入源IP和目的IP。最后链路层把整个IP数据报再包成以太网帧加上目标MAC地址、源MAC地址和帧尾的校验序列然后把这一串比特通过网线或WiFi发出去。接收方收到数据后做相反的操作。链路层收帧、校验、去掉帧头帧尾把IP数据报交给网络层网络层解析IP头看到目的IP是本机就把TCP段上交传输层传输层检查端口号发现是80再把数据交给监听80端口的进程。这个过程叫解封装。这里有个核心观点每一层只关心自己的头部信息不解析上层的内容。就好比快递员只认面单上的收件地址绝不拆开包裹看你买的是什么东西。这个“越界”概念特别重要以后你用tcpdump抓包看到一帧数据里包含以太网头、IP头、TCP头、应用数据四层嵌套就会瞬间理解封装与解封装在物理世界里的样子。3. IP、端口与Socket网络编程的三块基石3.1 IP地址找到那台主机IP地址是网络层寻址的核心它回答的问题是“数据该发给哪台主机”。类比生活里IP地址就是门牌号。IPv4地址是一个32位二进制数习惯上写成四个十进制数比如192.168.1.100。一个局域网里的每台设备都要有自己的IP地址否则交换机不知道数据帧该往哪个口送。在这个基础上还有几个概念要认清。第一个是回环地址127.0.0.1它代表“本机自己”不管有没有网卡它都能通。开发调试时用127.0.0.1最方便因为数据不会真的跑到物理网络上纯粹在内核里转一圈就回来了。第二个是私网地址常见的有10.x.x.x、172.16.x.x到172.31.x.x、192.168.x.x。私网地址只能在内网使用不能直接上公网。家用路由器给手机、电脑分配的通常都是192.168.x.x这类地址设备要访问互联网路由器会做NAT网络地址转换把私网地址映射成公网地址。IPv4的总地址数约43亿在全球互联网面前早就捉襟见肘所以有了IPv6用128位地址彻底解决了数量问题。写网络程序时函数要优先考虑同时支持IPv4和IPv6比如用getaddrinfo而不是老旧的gethostbyname。不过初学阶段先在IPv4上跑通再去接触IPv6会更容易上手。3.2 端口号找到那个进程有了IP地址你可以找到一台主机但主机上同时跑着几十上百个进程。你连上服务器到底是要把数据交给Web服务器、SSH服务还是数据库这就轮到端口号登场。端口号是传输层用16位二进制数标识的范围从0到65535它回答的问题是“数据该交给这台机器上的哪个进程”。常见端口要记几个22是SSH80是HTTP443是HTTPS3306是MySQL6379是Redis。0到1023范围通常需要管理员权限才能监听1024到49151是可以自由使用的注册端口49152到65535一般用于动态分配。客户端发起连接时系统会自动分配一个临时端口作为源端口服务端收到的报文里源IP和源端口就是客户端的身份标识。一台服务器可以同时监听多个端口比如同一台机器上开着Nginx80、MySQL3306、Redis6379。而一个端口同一时刻只能被一个监听套接字占用这是新手一定会踩的坑后面专门讲。识别一条连接需要五个要素源IP、源端口、目的IP、目的端口、协议类型TCP还是UDP网络领域管这叫五元组。3.3 Socket用户程序与内核协议栈之间的门说完IP和端口终于来到Linux网络编程的核心对象Socket。Socket可以理解成“应用程序访问网络协议栈的大门”。你写的程序是用户态进程协议栈是内核的一部分两者之间靠Socket这座桥连接。创建Socket后内核会在协议栈里帮你分配一套收发缓冲区、状态标记和队列并返回一个文件描述符给你。这里要强调一个容易混淆的点Socket不是协议它是API是操作系统暴露给用户的网络入口。你通过这个入口调用内核的协议栈能力。Linux里一切皆文件Socket也不例外的体现为文件描述符所以你能用read、write来收发数据也能用close来关闭连接。创建Socket时最典型的三类参数地址族类型说明AF_INETSOCK_STREAM基于IPv4的TCP流式套接字AF_INETSOCK_DGRAM基于IPv4的UDP数据报套接字AF_UNIXSOCK_STREAM本机进程间通信的本地套接字TCP用流式Socket因为TCP天然是字节流有连接、可靠、按序UDP用数据报Socket每个报文独立没有连接概念适合音视频等允许少量丢失的场景。初学阶段把TCP的Socket流程跑熟再回头看UDP会轻松很多。记住这句话Socket是门IP是门牌端口是门后面的房间号协议栈是整栋大楼。4. 写代码前先练手用Linux命令触摸协议4.1 ss与netstat看一眼当前连接状态不少人的第一反应是上来写代码但我建议先学会用命令观察网络状态。Linux系统里最常用的连接查看命令是ss旧一点的系统用netstat。运行下面的命令ss -tan-t只看TCP-a看所有状态的连接-n跳过域名解析直接显示IP和端口。输出大致长这样State Recv-Q Send-Q Local Address:Port Peer Address:Port LISTEN 0 128 0.0.0.0:22 0.0.0.0:* ESTAB 0 0 192.168.1.100:22 192.168.1.50:52314 TIME_WAIT 0 0 192.168.1.100:22 192.168.1.80:50112这些状态词就是TCP协议的状态机LISTEN表示这个端口正在被某个进程监听等待客户端连接ESTABESTABLISHED表示一条TCP连接已经建立双方可以收发数据TIME_WAIT表示主动关闭连接的一方在等待一段时间确保迟到的报文在网络中消亡后才能彻底释放端口。初学者看到一堆TIME_WAIT别慌这是正常现象系统会在大约60秒后回收这些连接。如果你想知道某个端口被谁占用了用这个组合sudo ss -tlnp | grep 8080-p参数能显示占用端口的进程PID和名字。排查“端口被占用”这类问题时这条命令比翻代码管用得多。我见过太多次新手以为bind失败是代码写错了其实完全是上一个进程没退干净。4.2 tcpdump抓到真正的报文看看理论知识说得再多都不如亲眼看一次数据包来得通透。tcpdump是Linux下最经典的抓包工具它把经过网卡的真实报文打印出来。抓TCP连接的过程比如我们后面要跑的服务器监听8888端口可以这么抓sudo tcpdump -i any -nn tcp port 8888-i any抓所有网卡-nn不解析域名和端口名tcp port 8888只抓TCP协议且端口匹配8888的包。运行后发起一次连接你会看到类似这样的输出14:23:01.111111 IP 127.0.0.1.50000 127.0.0.1.8888: Flags [S], seq 1000 14:23:01.111222 IP 127.0.0.1.8888 127.0.0.1.50000: Flags [S.], seq 2000, ack 1001 14:23:01.111333 IP 127.0.0.1.50000 127.0.0.1.8888: Flags [.], ack 2001Flags [S]是SYN报文表示“我想建立连接”[S.]是SYNACK表示“同意你的连接请求”[.]是纯ACK表示“收到你的确认”。这三个包就是TCP三次握手的完整流程。第一次抓包看不懂没关系先混个眼熟知道协议真的是以这么直白的形式在网上跑就比很多只写CRUD的开发者高一个level了。抓包也可以保存成文件交给Wireshark做可视化分析sudo tcpdump -i any -w /tmp/capture.pcap tcp port 88884.3 ping与traceroute验证连通性和路由路径再介绍两个网络层工具。ping用来验证目标主机通不通原理是发送ICMP回显请求ping -c 4 www.example.com输出会显示每个ICMP报文的往返时间RTT和TTL。RTT大说明网络链路慢TTL值则能间接看出经过了多少跳路由器。如果你刚配置完Linux网络环境第一件事就应该是ping一下网关确认本机不在“孤立岛”上。traceroute更进一步它打印出数据包从本地到目标主机经过的每一跳路由器地址traceroute www.example.com这个工具的原理是利用IP头里的TTL字段第一次发一个TTL为1的报文第一跳路由器收到后发现TTL过期返回一个ICMP超时报文于是客户端就知道第一跳是谁然后TTL加1依次探测第二跳、第三跳。每次上网卡顿我用ping判断是不是链路问题用traceroute定位卡在哪一跳顺序清晰效率也高。5. 实战写一个最小的TCP回声服务5.1 环境准备与整体思路接下来的例子需要一台Linux环境以及gcc编译器。如果没有最简单的办法是装个虚拟机或者在Windows上启用WSL。注意虚拟机默认使用NAT网络模式外部机器不能直接访问虚拟机的某个端口这会影响到后面联调具体在第6章展开。这个demo的目标极简一个服务端监听8888端口接受一个客户端连接客户端连上后发送一串字符串服务端收到后原样返回。它不处理并发、不做协议解析、不考虑性能就是一个能跑通的最小闭环让你在动手层面感受完整流程socket、bind、listen、accept、connect、read、write、close。这不仅仅是“跑通就完事”整个过程中的每个系统调用都会让你确认前面讲的分层模型和协议状态是真实存在的。5.2 服务端socket、bind、listen、accept四件套下面是最简单的TCP服务端代码保存为server.c#include stdio.h #include stdlib.h #include string.h #include unistd.h #include arpa/inet.h #define PORT 8888 #define BACKLOG 5 int main() { int server_fd, client_fd; struct sockaddr_in server_addr, client_addr; socklen_t addr_len sizeof(client_addr); char buffer[1024]; // 1. 创建TCP套接字 server_fd socket(AF_INET, SOCK_STREAM, 0); if (server_fd 0) { perror(socket); exit(EXIT_FAILURE); } // 2. 允许地址复用避免TIME_WAIT导致bind失败 int opt 1; setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt)); // 3. 绑定IP和端口 memset(server_addr, 0, sizeof(server_addr)); server_addr.sin_family AF_INET; server_addr.sin_addr.s_addr htonl(INADDR_ANY); server_addr.sin_port htons(PORT); if (bind(server_fd, (struct sockaddr *)server_addr, sizeof(server_addr)) 0) { perror(bind); exit(EXIT_FAILURE); } // 4. 开始监听 if (listen(server_fd, BACKLOG) 0) { perror(listen); exit(EXIT_FAILURE); } printf(server listening on port %d\n, PORT); // 5. 接受客户端连接此处阻塞 client_fd accept(server_fd, (struct sockaddr *)client_addr, addr_len); if (client_fd 0) { perror(accept); exit(EXIT_FAILURE); } char *client_ip inet_ntoa(client_addr.sin_addr); printf(client connected: %s:%d\n, client_ip, ntohs(client_addr.sin_port)); // 6. 接收数据原样返回 ssize_t len read(client_fd, buffer, sizeof(buffer) - 1); if (len 0) { buffer[len] \0; printf(received: %s\n, buffer); const char *msg hello from server; write(client_fd, msg, strlen(msg)); } close(client_fd); close(server_fd); return 0; }逐行看几个关键点。socket(AF_INET, SOCK_STREAM, 0)创建的是IPv4的TCP套接字。AF_INET是地址族SOCK_STREAM表示流式传输0让内核自动选TCP协议。bind负责把套接字绑定到一个具体的IP和端口上。为什么需要htons(PORT)因为端口号在网络传输中要用大端字节序而x86等小端机器内部是小端htons把主机字节序转成网络字节序。同样htonl(INADDR_ANY)是因为INADDR_ANY表示“绑定本机所有可用IP”也需要转成网络字节序。listen(server_fd, BACKLOG)让套接字进入监听状态BACKLOG是未完成连接队列的最大长度。accept是阻塞调用没有客户端连上来时进程停在这里不动有连接到达后它返回一个新的套接字文件描述符client_fd这个新文件描述符才代表“当前这个客户端连接”原来的server_fd继续作为监听者等待下一拨连接。这里很容易误解不是accept返回的fd和监听fd同一个网络程序里一个fd对应一条连接连接结束fd就释放。5.3 客户端socket、connect、读写客户端的代码更短一些保存为client.c#include stdio.h #include stdlib.h #include string.h #include unistd.h #include arpa/inet.h #define PORT 8888 int main(int argc, char *argv[]) { if (argc ! 2) { printf(usage: %s server_ip\n, argv[0]); exit(EXIT_FAILURE); } int sock_fd; struct sockaddr_in server_addr; char buffer[1024]; // 1. 创建TCP套接字 sock_fd socket(AF_INET, SOCK_STREAM, 0); if (sock_fd 0) { perror(socket); exit(EXIT_FAILURE); } // 2. 设置服务器地址 memset(server_addr, 0, sizeof(server_addr)); server_addr.sin_family AF_INET; server_addr.sin_port htons(PORT); // 3. 把点分十进制IP转成二进制 if (inet_pton(AF_INET, argv[1], server_addr.sin_addr) 0) { perror(inet_pton); exit(EXIT_FAILURE); } // 4. 发起连接内核帮你完成三次握手 if (connect(sock_fd, (struct sockaddr *)server_addr, sizeof(server_addr)) 0) { perror(connect); exit(EXIT_FAILURE); } // 5. 发送数据 const char *msg hello from client; write(sock_fd, msg, strlen(msg)); // 6. 读取服务器返回的数据 ssize_t len read(sock_fd, buffer, sizeof(buffer) - 1); if (len 0) { buffer[len] \0; printf(server says: %s\n, buffer); } close(sock_fd); return 0; }inet_pton把127.0.0.1这种字符串形式的IP转成网络字节序的二进制结构比老旧的inet_addr更安全。connect成功返回意味着三次握手已经完成这是TCP非常关键的性质连接是经过双方确认的而不是客户端单方面的幻想。connect之后TCP的序列号、窗口大小都已经协商好可以直接收发数据。完整流程里客户端不需要bind系统会自动分配一个临时端口不需要listen因为客户端不是被连接方。5.4 编译、运行、抓包验证三次握手编译运行gcc server.c -o server gcc client.c -o client ./server ./client 127.0.0.1服务端会打印client connected: 127.0.0.1:xxxxx客户端会打印server says: hello from server。read和write在这个例子里用起来像操作文件一样这也印证了Linux“一切皆文件”的设计哲学。Socket确实是文件描述符对连接的读写就是对文件描述符的读写。跑通了只是第一步建议你按上一节的方法开启抓包再跑一次程序亲眼看到SYN、SYNACK、ACK三个报文依次出现。这个步骤虽然琐碎但效果非常好你会发现TCP握手不是一个抽象概念而是实实在在出现在网卡上的三个数据包。抓包时如果用-nn参数IP地址不会被解析成域名所以127.0.0.1会直接显示方便对照。5.5 这个demo暴露出的问题这个程序能跑但它极其简陋局限也很明显第一它一次只能处理一个客户端。accept之后如果第二个客户端连上来谁都不理它因为程序正阻塞在第一个连接的read上。第二read是阻塞式的如果客户端一直不发数据服务端就永远卡在那一行。第三收发没有消息边界如果客户端连续发两条消息服务端可能一次read读完也可能分三次读完怎么切分边界需要应用层协议来定义。这些不是代码写得不好而是所有TCP网络程序都要面对的基础问题。处理它们要靠多线程、非阻塞IO、IO多路复用select/poll/epoll这些技术这是本系列后面几个篇章的核心内容。现在先把这套流程跑通肚子里有了一张完整的图再学那些高级玩法就有参照系了。6. 新手期最容易踩的四个坑6.1 bind报错Address already in use我敢说每个写TCP服务端的人都遇见过这个错误。表现是第二次启动程序时bind返回失败错误信息为Address already in use。通常原因不是端口真的被别的服务占用了而是上一个程序正常退出了但TCP连接还在TIME_WAIT状态系统要等约60秒才能释放端口。TIME_WAIT产生的原因是被动关闭连接的一方可能还有数据没发完主动关闭方要留出时间处理迟到的报文。解决这个问题的标准做法就是我们在服务端代码里写的那行setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt))。这行代码在bind之前设置告诉内核即使有连接处于TIME_WAIT这个端口也可以立即重用。很多人把它当模板抄却不理解为什么这里必须说清楚没有它你每次调试都要干等一分钟有它在你的开发体验会顺畅很多。6.2 虚拟机里连不上别急着怀疑代码如果你的服务端跑在虚拟机里客户端从宿主机连接连不上是最常见的问题。这里面的原因通常不是socket代码而是虚拟机网络模式。VMware默认的NAT模式下虚拟机有自己独立的NAT网段宿主机不能直接访问虚拟机。如果你要从宿主机连虚拟机的8888端口要么把网络模式改为桥接bridge让虚拟机和宿主机处于同一局域网要么配置NAT端口转发。判断问题是不是出在网络环境有一个很省事的办法在虚拟机内部自己连自己比如客户端也跑在虚拟机里连127.0.0.1。如果能通说明代码没有问题剩下的就是网络拓扑问题。另外还要检查Linux防火墙某些发行版默认开启firewalld可以暂时关闭或者放行端口sudo systemctl stop firewalld不需要一上来就怀疑协议栈先看连通性再看端口监听最后看代码这个排查顺序能帮你省掉大量时间。6.3 数据收不齐TCP流式协议没有消息边界写网络程序最普遍的困惑就是为什么我的read读出来的数据不完整或者一次性读到了两条消息。原因是TCP是流式协议它只保证字节按序到达但不保证字节的“组合方式”。你可以把TCP想成一根水管水连续地流你从水管里接水接多少取决于你拿杯子的时机而不是取决于谁往水管里倒了多少水。应用层如果不好好定义消息边界就会出现粘包把两条消息合成一条读和半包一条消息被拆成两次读。解决思路有很多最常见的是这几种固定长度消息每条消息长度一样读完一个长度再读下一个分隔符协议用\r\n或自定义分隔符切分消息长度前缀每个消息前面用几个字节声明消息体长度接收方先读长度再读对应长度的正文。这已经属于应用层协议设计的范畴第一篇文章不展开但你要先有这个意识光靠TCP自带机制无法解决业务消息的分界问题。6.4 单线程阻塞带来的并发瓶颈最后一个坑也是从入门到进阶的必经坎阻塞IO。accept会阻塞read会阻塞意味着一个单线程程序同一时间只能服务一个连接。很多新手在这里会把程序改成多线程为每个连接开一个线程但线程多了以后又有资源竞争和上下文切换开销所以业界主流做法是用IO多路复用select/poll/epoll配合非阻塞IO用少量线程管理大量连接。第一篇文章不谈实现细节只给你一个宏观认知网络编程的下半场是从“能通”走向“能扛”的过程。当你的程序从单连接进化到支持成千上万并发连接时你对epoll、事件驱动、用户态协议栈这些词的理解就会完全不一样。现在先把单连接的demo吃透后面的路一步一步走。我当年第一次把这个demo跑通时兴冲冲给一台远程服务器发消息结果客户端一直卡住不动最后发现是对端根本没启动服务而不是代码问题。从那以后我养成了一个习惯先抓包、再猜原因。这个习惯一直用到现在。如果你也是刚迈进Linux网络编程的门槛建议把tcpdump这类工具用熟让真实的数据包成为你排查问题的第一依据。等脑子里有了这张协议运行的全局图再看后续的并发处理、非阻塞IO、epoll一切都会顺理成章。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →