十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C/C++网络编程实战:从Socket基础到TCP/UDP协议选择与并发处理

C/C++网络编程实战:从Socket基础到TCP/UDP协议选择与并发处理 1. 从“Hello World”到网络世界为什么我们需要Socket如果你写过C或C第一个程序大概率是向屏幕打印“Hello World”。那是一个程序与本地控制台的对话。而网络编程本质上就是教会你的程序如何与世界上另一台计算机上的另一个程序对话。Socket套接字就是这个对话的“电话”。今天我们不谈那些复杂的网络协议栈就从最实际的角度出发聊聊怎么用C/C这门“老手艺”打好TCP和UDP这两通至关重要的“电话”。很多人觉得网络编程门槛高充斥着bind、listen、accept、connect这些晦涩的函数还有各种令人头疼的错误码。但究其核心它解决的场景非常朴素数据如何从A点可靠或高效地到达B点。比如你玩在线游戏时的实时位置同步可能用UDP你刷网页时加载的文章内容一定用TCP甚至你手机APP里那个不停转圈的小图标背后都是一次次Socket通信在忙碌。C/C作为系统级编程语言直接操作Socket API给了我们最底层的控制力。这意味着你能精确地管理每一个字节的发送时机、缓冲区的每一寸空间也能在最苛刻的性能场景下如高频交易、游戏服务器榨干硬件的最后一分潜力。当然这也意味着你需要亲手处理更多细节比如连接的生命周期、数据的边界、以及各种网络异常。这就像手动挡汽车操控感强但需要更多的驾驶技巧。接下来的内容我会假设你已经有基本的C/C语法和指针知识。我们将绕过教科书式的协议分层讲解直接进入实战。我会用具体的代码示例对比TCP和UDP在行为上的根本差异并分享一些我从实际项目里踩坑换来的经验——比如为什么你的TCP服务端突然卡住或者UDP数据怎么就“丢”得无影无踪了。我们的目标是看完之后你不仅能写出可以跑通的网络程序更能理解每一行代码背后的网络正在发生什么从而写出更健壮、更高效的代码。2. TCP vs UDP不是“好与坏”而是“合适与不合适”几乎所有网络编程的入门都会比较TCP和UDP但很多对比停留在“TCP可靠、UDP不可靠”的表面。这种说法容易让人误解仿佛UDP是个“残次品”。实际上它们是设计哲学完全不同的两种工具选择哪一种完全取决于你的应用场景最需要什么。我们可以用一个生活中的类比来理解TCP像打电话UDP像发邮政明信片。打电话TCP首先你需要拨号三次握手建立连接确认对方在线并能接听。通话过程中你会说“喂你听到吗”来确认ACK确认机制如果某句话没听清你会要求对方重复超时重传。通话结束后你们会道别四次挥手断开连接。整个过程是面向连接、可靠、有序的字节流。代价是额外的延迟和开销。发明信片UDP你写好内容贴上邮票目标地址和端口扔进邮筒。你不会知道对方是否收到无连接也不知道明信片是否按你寄出的顺序到达可能乱序甚至明信片可能中途丢失可能丢包。但它的好处是极其简单和快速。基于这个核心区别我们可以从几个维度进行技术性对比特性维度TCP (传输控制协议)UDP (用户数据报协议)连接性面向连接。通信前必须建立虚拟链路。无连接。直接发送无需握手。可靠性高可靠。通过确认、重传、校验和等机制保证数据正确、不丢失、不重复、按序到达。不可靠。不保证交付不保证顺序可能重复。数据形式字节流Stream。没有消息边界。发送端多次write的数据接收端可能一次read就全部收到。数据报Datagram。保留消息边界。每次sendto发送的是一个完整的包接收端recvfrom也必须以一个完整的包为单位接收。头部开销较大通常20字节包含序列号、确认号、窗口等复杂控制信息。极小仅8字节只有源/目标端口、长度和校验和。传输效率相对较低。建立连接有延迟确认重传机制增加开销拥塞控制会主动降低发送速率。非常高。几乎没有额外控制开销发送速率完全由应用层控制。流量控制有。通过滑动窗口机制防止发送方淹没接收方。无。发送方可以任意速率发送可能导致接收方缓冲区溢出丢包。拥塞控制有。通过慢启动、拥塞避免等算法适应网络状况避免全局崩溃。无。可能加剧网络拥塞。典型应用场景需要高可靠性的场景HTTP/HTTPS网页、FTP文件传输、SMTP邮件、数据库连接。需要低延迟、可容忍部分丢失的场景视频直播、语音通话、在线游戏、DNS查询、DHCP。一个关键的心智模型理解“字节流”和“数据报”的区别是避免TCP编程踩坑的关键。TCP的接收方就像一个水龙头下的水桶发送方是往水管里倒水。你无法区分倒进来的水是哪一瓢。而UDP的接收方就像一个个快递柜每个包裹数据报都是独立且完整的。所以当你选择协议时应该问自己我的数据更怕“延迟”还是更怕“丢失”一封重要的合同丢一个字都可能出问题但晚几秒收到没关系 -选TCP。一场直播球赛画面卡顿、声音断续延迟比偶尔花屏丢包更影响体验 -选UDP。3. 核心API全景与一个TCP“回显”服务器的实现无论是TCP还是UDPSocket编程都围绕着一组核心的系统调用在Windows上是Winsock API概念类似。我们先鸟瞰全景再动手实现一个最简单的TCP服务器。3.1 Socket API 核心函数地图下图描绘了一个典型的TCP客户端/服务器交互流程以及UDP的简单流程展示了核心API的调用顺序和关系flowchart TD A[TCP 服务器端] -- B1[socket()br创建监听套接字]; B1 -- B2[bind()br绑定IP与端口]; B2 -- B3[listen()br开始监听]; B3 -- B4[accept()br阻塞等待连接]; C[TCP 客户端端] -- D1[socket()br创建连接套接字]; D1 -- D2[connect()br发起连接]; B4 -- 连接建立 -- E[新套接字br用于通信]; D2 -- 连接建立 -- E; E -- F1[send()/write()br发送数据]; E -- F2[recv()/read()br接收数据]; F1 F2 -- G[close()br关闭连接]; H[UDP 端] -- I1[socket()br创建数据报套接字]; I1 -- I2[bind()br可选绑定端口]; I2 -- I3[sendto()/recvfrom()br发送/接收数据报]; I3 -- I4[close()br关闭套接字];3.2 实现一个TCP回显服务器Echo Server回显服务器是网络编程的“Hello World”客户端发送什么服务器就原样发回什么。我们来实现一个支持单个客户端的阻塞式版本。服务器端代码 (tcp_echo_server.c)#include stdio.h #include stdlib.h #include string.h #include unistd.h #include arpa/inet.h // 主要Socket API和数据结构 #include sys/socket.h #define PORT 8080 #define BUFFER_SIZE 1024 int main() { int server_fd, new_socket; struct sockaddr_in address; int addrlen sizeof(address); char buffer[BUFFER_SIZE] {0}; // 1. 创建Socket文件描述符 // AF_INET: IPv4, SOCK_STREAM: TCP协议, 0: 默认协议 if ((server_fd socket(AF_INET, SOCK_STREAM, 0)) 0) { perror(socket failed); exit(EXIT_FAILURE); } // 2. 配置服务器地址结构 address.sin_family AF_INET; address.sin_addr.s_addr INADDR_ANY; // 绑定到本机所有IP address.sin_port htons(PORT); // 端口号htons将主机字节序转为网络字节序 // 3. 绑定Socket到指定IP和端口 if (bind(server_fd, (struct sockaddr *)address, sizeof(address)) 0) { perror(bind failed); close(server_fd); exit(EXIT_FAILURE); } // 4. 开始监听等待连接请求 // 第二个参数 backlog: 等待连接队列的最大长度 if (listen(server_fd, 3) 0) { perror(listen); close(server_fd); exit(EXIT_FAILURE); } printf(Server listening on port %d\n, PORT); // 5. 接受一个传入的连接 // accept会阻塞直到有客户端连接进来 if ((new_socket accept(server_fd, (struct sockaddr *)address, (socklen_t*)addrlen)) 0) { perror(accept); close(server_fd); exit(EXIT_FAILURE); } printf(Client connected.\n); // 6. 通信循环读数据 - 回写数据 int valread; while ((valread read(new_socket, buffer, BUFFER_SIZE)) 0) { printf(Received: %s\n, buffer); // 将收到的数据原样发回 send(new_socket, buffer, valread, 0); memset(buffer, 0, BUFFER_SIZE); // 清空缓冲区 } // 7. 连接关闭或读取出错 if (valread 0) { printf(Client disconnected.\n); } else { perror(read error); } // 8. 清理资源 close(new_socket); close(server_fd); return 0; }客户端代码 (tcp_echo_client.c)#include stdio.h #include stdlib.h #include string.h #include unistd.h #include arpa/inet.h #include sys/socket.h #define PORT 8080 #define BUFFER_SIZE 1024 int main() { int sock 0; struct sockaddr_in serv_addr; char buffer[BUFFER_SIZE] {0}; char *hello Hello from client; // 1. 创建Socket if ((sock socket(AF_INET, SOCK_STREAM, 0)) 0) { perror(Socket creation error); return -1; } serv_addr.sin_family AF_INET; serv_addr.sin_port htons(PORT); // 2. 将IP地址从文本转换为二进制形式 if (inet_pton(AF_INET, 127.0.0.1, serv_addr.sin_addr) 0) { perror(Invalid address/ Address not supported); close(sock); return -1; } // 3. 发起连接 if (connect(sock, (struct sockaddr *)serv_addr, sizeof(serv_addr)) 0) { perror(Connection Failed); close(sock); return -1; } // 4. 发送数据 send(sock, hello, strlen(hello), 0); printf(Hello message sent\n); // 5. 读取回显数据 int valread read(sock, buffer, BUFFER_SIZE); printf(Echo from server: %s\n, buffer); // 6. 关闭连接 close(sock); return 0; }编译与运行在Linux或macOS终端下# 编译服务器 gcc tcp_echo_server.c -o server # 编译客户端 gcc tcp_echo_client.c -o client # 在一个终端运行服务器 ./server # 在另一个终端运行客户端 ./client如果一切正常服务器会打印“Client connected.”和接收到的消息客户端会打印从服务器回显的消息。3.3 代码关键点解析与避坑指南字节序转换htons()(host to network short) 和htonl()(host to network long) 是必须的。因为不同CPU架构如x86和ARM存储多字节数据的顺序大端/小端可能不同网络传输统一使用大端字节序。忘记转换会导致端口和IP地址解析错误。错误处理每一个Socket API调用后都必须检查返回值。这是C/C网络编程的纪律。perror可以打印出人类可读的错误原因。INADDR_ANY服务器绑定INADDR_ANY意味着监听所有本地网卡IP。如果你想只监听特定IP如某个内网网卡可以使用inet_pton转换后赋值。accept的返回值accept成功会返回一个新的Socket文件描述符(new_socket)。这个新的套接字才是用于和客户端通信的通道。原始的server_fd继续用于监听新的连接。这是实现并发服务器的关键。read的返回值read返回0表示对端关闭了连接收到了FIN包返回-1表示出错大于0才是读到的字节数。这是判断连接状态的核心依据。缓冲区与消息边界注意我们send时长度参数是valread实际读到的字节数而不是BUFFER_SIZE或strlen(buffer)。因为TCP是字节流我们回显的必须是原封不动的字节数。这也是处理TCP“粘包”问题的雏形——应用层需要自己定义消息边界例如在每个消息前加一个长度头。这个例子是阻塞式的服务器一次只能服务一个客户端。在实际生产中我们需要使用fork多进程、pthread多线程、select/poll/epollI/O多路复用或异步I/O等技术来处理并发。这是网络编程进阶的必经之路。4. UDP编程实战一个简单的消息发送与接收示例相比于TCP的“连线”通话UDP编程更像是在“喊话”。我们来看一个最简单的UDP发送端和接收端例子。接收端 (UDP Server) - udp_receiver.c接收端需要先绑定一个端口然后等待数据。#include stdio.h #include stdlib.h #include string.h #include unistd.h #include arpa/inet.h #define PORT 8888 #define BUFFER_SIZE 1024 int main() { int sockfd; struct sockaddr_in serv_addr, cli_addr; socklen_t cli_len sizeof(cli_addr); char buffer[BUFFER_SIZE]; // 1. 创建数据报Socket (SOCK_DGRAM) if ((sockfd socket(AF_INET, SOCK_DGRAM, 0)) 0) { perror(socket creation failed); exit(EXIT_FAILURE); } memset(serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family AF_INET; serv_addr.sin_addr.s_addr INADDR_ANY; serv_addr.sin_port htons(PORT); // 2. 绑定Socket到端口对于接收方bind是必须的 if (bind(sockfd, (const struct sockaddr *)serv_addr, sizeof(serv_addr)) 0) { perror(bind failed); close(sockfd); exit(EXIT_FAILURE); } printf(UDP Server listening on port %d\n, PORT); while(1) { // 3. 接收数据报 // recvfrom会阻塞直到有数据到来。它会告诉我们数据是谁发的。 int n recvfrom(sockfd, (char *)buffer, BUFFER_SIZE, 0, (struct sockaddr *)cli_addr, cli_len); if (n 0) { perror(recvfrom failed); continue; } buffer[n] \0; // 确保字符串结束因为recvfrom不保证 char cli_ip[INET_ADDRSTRLEN]; inet_ntop(AF_INET, (cli_addr.sin_addr), cli_ip, INET_ADDRSTRLEN); printf(Received from %s:%d - %s\n, cli_ip, ntohs(cli_addr.sin_port), buffer); // 4. 可选发送一个回复 char *reply Message received!; sendto(sockfd, reply, strlen(reply), 0, (const struct sockaddr *)cli_addr, cli_len); } close(sockfd); // 实际上循环不会退出这里仅示意 return 0; }发送端 (UDP Client) - udp_sender.c发送端无需连接直接向目标地址发送即可。#include stdio.h #include stdlib.h #include string.h #include unistd.h #include arpa/inet.h #define PORT 8888 #define BUFFER_SIZE 1024 int main() { int sockfd; struct sockaddr_in serv_addr; char buffer[BUFFER_SIZE]; const char *message Hello UDP Server!; // 1. 创建数据报Socket if ((sockfd socket(AF_INET, SOCK_DGRAM, 0)) 0) { perror(socket creation failed); exit(EXIT_FAILURE); } memset(serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family AF_INET; serv_addr.sin_port htons(PORT); // 2. 指定目标服务器地址这里用本地回环地址测试 if (inet_pton(AF_INET, 127.0.0.1, serv_addr.sin_addr) 0) { perror(Invalid address); close(sockfd); exit(EXIT_FAILURE); } // 3. 发送数据报 int n sendto(sockfd, message, strlen(message), 0, (const struct sockaddr *)serv_addr, sizeof(serv_addr)); if (n 0) { perror(sendto failed); close(sockfd); return -1; } printf(Message sent.\n); // 4. 等待接收回复可选 socklen_t len sizeof(serv_addr); n recvfrom(sockfd, (char *)buffer, BUFFER_SIZE, 0, (struct sockaddr *)serv_addr, len); if (n 0) { buffer[n] \0; printf(Server reply: %s\n, buffer); } close(sockfd); return 0; }4.1 UDP编程的核心要点与陷阱无连接性UDP没有connect、listen、accept。通信双方是对等的。通常先启动的一方接收方需要bind一个端口发送方直接sendto。sendto和recvfrom这两个函数是UDP通信的核心。每次发送都必须指定目标地址每次接收都能得到发送方的地址。这使得UDP非常适合广播和多播。消息边界recvfrom接收到的数据大小就是对方一次sendto发送的数据大小只要不超过缓冲区。如果发送的数据报大于接收缓冲区多余的部分会被静默丢弃。这是UDP和TCP最大的行为差异之一。可靠性缺失sendto成功只表示数据已交给操作系统网络栈不保证对方收到。recvfrom会一直阻塞直到有数据报到来但不知道下一个数据报何时来、会不会来。应用层必须自己处理丢包、乱序和重复问题。常见的方案是在应用层添加序列号、确认和重传逻辑例如实现一个简单的RDT协议或者使用像QUIC这样基于UDP的可靠传输协议。缓冲区与性能由于UDP没有流量和拥塞控制如果发送速率超过网络链路容量或接收方处理能力会导致大量丢包。发送方需要根据应用场景如视频编码率自行控制发送节奏。5. 从阻塞到并发处理多个客户端的实战策略我们之前的TCP例子是阻塞且单线程的accept和read都会阻塞整个进程。这在现实中是不可用的。下面介绍几种主流的并发处理模型。5.1 多进程模型fork这是最传统的方法。主进程负责accept每当有新连接时就fork出一个子进程来处理这个连接的IO主进程继续等待新连接。// 伪代码示例 int main() { listen_fd socket(...); bind(...); listen(...); while(1) { conn_fd accept(listen_fd, ...); pid_t pid fork(); if (pid 0) { // 子进程 close(listen_fd); // 子进程不需要监听socket handle_client(conn_fd); // 处理客户端请求 close(conn_fd); exit(0); } else { // 父进程 close(conn_fd); // 父进程不需要连接socket // 注意需要回收子进程资源避免僵尸进程 } } }优点编程模型简单进程间隔离性好一个客户端崩溃不影响服务器主进程和其他客户端。缺点创建进程开销大上下文切换成本高进程间通信IPC复杂支持的并发连接数受限于系统进程数。5.2 多线程模型pthread与多进程类似但使用线程。主线程accept创建新线程处理连接。// 伪代码示例 void* client_thread(void* arg) { int conn_fd *(int*)arg; free(arg); handle_client(conn_fd); close(conn_fd); return NULL; } int main() { listen_fd socket(...); bind(...); listen(...); while(1) { conn_fd accept(listen_fd, ...); int *pconn malloc(sizeof(int)); *pconn conn_fd; pthread_t tid; pthread_create(tid, NULL, client_thread, pconn); pthread_detach(tid); // 分离线程使其结束后自动释放资源 } }优点创建和切换开销比进程小共享数据方便全局变量。缺点需要处理线程同步锁一个线程崩溃可能影响整个进程如内存越界大量线程时调度开销依然显著。5.3 I/O多路复用I/O Multiplexing这是高性能网络服务器的基石。核心思想是一个线程或少量线程管理多个Socket文件描述符通过系统调用监控这些描述符的状态是否可读、可写、出错当有事件发生时再进行处理。这避免了为每个连接创建一个线程/进程的巨大开销。select/poll早期方案。select通过fd_set位图来管理描述符有数量限制通常是1024且每次调用需要在内核和用户空间之间复制整个描述符集合效率随连接数线性下降。poll使用pollfd结构数组没有硬性数量限制但同样存在线性扫描和复制开销。epoll (Linux特有)现代Linux高性能服务器的首选。边缘触发(ET)与水平触发(LT)ET模式只在描述符状态变化时通知一次要求应用程序必须一次性读完所有数据LT模式在状态就绪时持续通知编程更简单但可能效率稍低。工作流程epoll_create创建一个epoll实例。epoll_ctl向实例中添加、修改或删除需要监控的fd及其关注的事件如EPOLLIN可读。epoll_wait等待事件发生。返回发生事件的fd数组应用程序遍历这个通常很小的数组进行处理效率是O(1)。// 一个简化的epoll LT模式服务器框架 #define MAX_EVENTS 64 int main() { int listen_fd setup_listen_socket(); int epoll_fd epoll_create1(0); struct epoll_event ev, events[MAX_EVENTS]; // 监听listen_fd上的可读事件新连接 ev.events EPOLLIN; ev.data.fd listen_fd; epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, ev); while(1) { int nfds epoll_wait(epoll_fd, events, MAX_EVENTS, -1); // 阻塞等待 for (int i 0; i nfds; i) { if (events[i].data.fd listen_fd) { // 处理新连接 int conn_fd accept(listen_fd, ...); setnonblocking(conn_fd); // 通常设置为非阻塞 ev.events EPOLLIN | EPOLLET; // 监听可读边缘触发 ev.data.fd conn_fd; epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn_fd, ev); } else { // 处理已连接套接字上的数据 handle_client_event(events[i].data.fd); } } } }优点能够用少量线程支撑数万甚至数十万的并发连接资源利用率极高。缺点编程模型复杂尤其是ET模式需要小心处理逻辑都在单线程内如果某个事件处理阻塞会影响整个服务。5.4 异步I/O与Proactor模式真正的异步I/O如Linux的aio_*系列函数或Windows的IOCP是更高级的模式。它由内核发起I/O操作操作完成后主动通知应用程序。应用程序只需提交请求和设置回调。这种模式Proactor与epoll等Reactor在事件驱动层面有区别性能潜力更大但API更复杂在Linux上成熟度不如epoll。选择建议对于学习和小型服务多线程模型足够。对于需要高并发的生产级服务Linux下epoll是绝对的主流选择。理解并掌握epoll是进阶C/C网络编程的必备技能。6. 生产环境中的核心议题与调试技巧写出能跑通的代码只是第一步。要让程序在网络环境这个“战场”上稳定运行你需要关注更多。6.1 粘包与拆包TCP特有这是TCP字节流特性带来的经典问题。假设客户端连续发送两条消息“Hello”和“World”服务器一次read可能收到“HelloWorld”也可能分两次收到“Hel”和“loWorld”。解决方案在应用层定义消息边界。定长消息每条消息固定长度不足补位。简单但浪费带宽。分隔符用特殊字符如\n分隔消息。需要转义分隔符本身。长度前缀最常用的方法。在消息头部添加一个固定长度的字段如2字节的uint16_t表示后面消息体的长度。接收方先读长度头再读取指定长度的消息体。// 发送端伪代码 uint16_t msg_len htons(strlen(message)); send(sock, msg_len, sizeof(msg_len), 0); // 先发长度 send(sock, message, strlen(message), 0); // 再发内容 // 接收端伪代码 uint16_t msg_len_net; read(sock, msg_len_net, sizeof(msg_len_net)); uint16_t msg_len ntohs(msg_len_net); char* buffer malloc(msg_len 1); read(sock, buffer, msg_len); // 读取指定长度的内容 buffer[msg_len] \0;6.2 心跳与保活TCP连接在空闲时中间的路由器或防火墙可能会因为超时将其断开。为了检测对端是否存活、保持连接活跃需要引入心跳机制。应用层心跳双方定期如30秒向对方发送一个特殊的小数据包心跳包。如果一段时间如90秒没收到对方的心跳则认为连接已断进行重连或清理。TCP Keepalive操作系统提供的TCP保活选项。可以设置空闲多久后开始发送探测包间隔多久发一次发多少次后放弃。但默认时间通常很长小时级且只能检测TCP层是否连通不能检测应用层是否“活着”。生产环境通常使用应用层心跳。6.3 非阻塞I/O与EAGAIN/EWOULDBLOCK在高性能服务器中Socket通常被设置为非阻塞模式fcntl(fd, F_SETFL, O_NONBLOCK)。当read/write/accept/connect等操作不能立即完成时会返回-1并设置errno为EAGAIN或EWOULDBLOCK意思是“请稍后再试”。 这要求你的代码必须能正确处理这种“暂时失败”在epoll_wait返回事件后进行循环读取或循环写入直到返回EAGAIN确保一次性处理完所有就绪的数据。// 非阻塞读示例 ssize_t read_all(int fd, char* buf, size_t len) { ssize_t total_read 0; while (total_read len) { ssize_t n read(fd, buf total_read, len - total_read); if (n 0) { total_read n; } else if (n 0) { return 0; // EOF } else if (errno EAGAIN || errno EWOULDBLOCK) { break; // 暂时没数据了 } else { return -1; // 真实错误 } } return total_read; }6.4 常用调试与排查工具netstat/ss查看系统上的Socket连接状态LISTEN, ESTABLISHED, TIME_WAIT等、监听端口、统计信息。ss是netstat的现代替代速度更快。netstat -tulnp | grep :8080 # 查看谁在监听8080端口 ss -tan | grep ESTAB # 查看所有TCP连接tcpdump/Wireshark网络抓包分析的黄金组合。tcpdump是命令行工具可以抓取指定网卡、端口、协议的数据包并保存为文件。Wireshark是图形化工具能对抓包文件进行深度解析直观看到TCP三次握手、数据包内容、重传等。sudo tcpdump -i any port 8080 -w capture.pcap # 抓取8080端口流量lsof列出打开的文件。在网络编程中文件描述符就是“文件”。可以用来查看某个进程打开了哪些Socket。lsof -i :8080 # 查看使用8080端口的进程 lsof -p PID # 查看指定进程打开的所有文件包括socketstrace跟踪进程的系统调用。当你的程序行为诡异时可以用它看看read、write、connect等系统调用到底发生了什么参数和返回值是什么。strace -f -e tracenetwork ./your_server # 跟踪服务器所有网络相关系统调用网络编程是一个既深且广的领域从最基础的Socket API到构建支撑百万并发的分布式系统中间有无数的细节和挑战。本文希望能为你打下坚实的实践基础并建立起正确的概念模型。记住理解协议的行为TCP的流与可靠UDP的报与不可靠比死记API更重要。多写代码多抓包观察多思考“如果网络断了会怎样”、“如果对方发得很快会怎样”你才能真正掌握这门与机器和世界对话的艺术。
返回列表