一. 计算机网络概述
1.1 TCP/IP 五层模型
| 5 | 应用层 | 应用程序使用互联网的规程 |
| 4 | 传输层 | TCP/UDP;进程到进程的通信、流量控制、差错控制 |
| 3 | 互联网层 | IP;不可靠、无连接、尽最大努力交付;32 位 IP 地址 |
| 2 | 数据链路层 | 组帧与传帧(帧是传输单位) |
| 1 | 物理层 | 硬件设备的电气特性规范 |
收发流程:发送时自上而下 插口层 → 协议层(加 TCP/UDP 头 → 加 IP 头)→ 接口层(加以太网头)→ 网卡;接收时反向逐层剥离。
1.2 TCP vs UDP
| 连接 | 面向连接(三次握手) | 无连接 |
| 可靠性 | 可靠(确认 + 超时重传 + 滑动窗口流控) | 不可靠,无流控无确认 |
| 适用 | 一般数据传输 | 简单请求-响应、网络条件好 |
| 效率 | 有连接开销 | 快,但差错/流控要应用自己做 |
- 用端口(16 位)区分进程,连接由端点 (host, port) 标识;同一台机器上各进程端口必须不同。
1.3 C/S 模型
客户端负责与用户交互并展示结果,服务器端负责处理请求与业务逻辑、不提供界面。本章例子基本都是 C/S。
二. Socket 编程
2.1 基本 API
| socket() | 双方 | 创建套接字(返回整型描述符) |
| bind() | 服务器 | 绑定到本机 IP + 端口 |
| listen() | 服务器 | 进入监听,backlog 是请求队列长度 |
| accept() | 服务器 | 取出队首连接请求,阻塞等待 |
| connect() | 客户端 | 发起连接(阻塞到三次握手完成或超时) |
| send()/write() | 双方 | 发送 |
| recv()/read() | 双方 | 接收 |
| close() | 双方 | 释放 |
socket
int socket(int domain, int type, int protocol);
| domain | AF_INET(IPv4,最常用)/AF_INET6/AF_UNIX(本机进程间) |
| type | SOCK_STREAM(TCP)/SOCK_DGRAM(UDP)/SOCK_RAW(原始套接字) |
| protocol | 一般 0 |
bind + 地址结构
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
struct sockaddr_in {
unsigned short sin_family; // AF_INET
unsigned short sin_port; // 端口号(网络字节序)
struct in_addr sin_addr; // IP 地址
unsigned char sin_zero[8]; // 填 0,使与 sockaddr 等长
};
struct in_addr { unsigned long s_addr; };
传参时要强转成 (struct sockaddr *)。常用 my_addr.sin_addr.s_addr = INADDR_ANY;(本机任意 IP)。
listen / accept / connect
int listen(int sockfd, int backlog);
int accept(int sockfd, struct sockaddr *addr, int *addrlen); // 返回新的连接 socket(最低可用 fd)
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
- ⚠ accept 被信号中断(EINTR)或非阻塞无连接(EAGAIN)时,重新调用即可。
- ⚠ connect 失败后该 socket 不可再用,必须 close 后重新 socket。
收发
int send(int sockfd, char *buff, int nbytes, int flags);
int sendto(int sockfd, char *buff, int nbytes, int flags, struct sockaddr *to, int addrlen);
int recv(int sockfd, char *buff, int nbytes, int flags);
int recvfrom(int sockfd, char *buff, int nbytes, int flags, struct sockaddr *from, int addrlen);
send/recv 用于已连接的 socket;sendto/recvfrom 带对方地址,UDP 用。
flags:MSG_OOB 带外数据/MSG_PEEK 窥视/MSG_DONTROUTE 绕过路由。
⚠ 对已关闭的连接写会收到 SIGPIPE(默认终止进程),可设 MSG_NOSIGNAL。
2.2 字节序与地址转换
- 网络字节序 = 大端;主机可能小端,必须转换。
uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); // 主机 → 网络
uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort); // 网络 → 主机
| inet_addr(const char *cp) | 点分十进制 → 网络字节序二进制 |
| inet_aton(const char *cp, struct in_addr *inp) | 同上,结果存入 inp |
| inet_ntoa(struct in_addr in) | 网络字节序 → 点分十进制字符串 |
| inet_network(const char *cp) | → 主机字节序 32 位 |
| inet_lnaof(struct in_addr) | 取主机号 |
| inet_netof(struct in_addr) | 取网络号 |
10.20.30.40 对照
| 281E140A | 0A141E28 | 141E28 | 0A |
⚠ addrstr.s_addr++ 改的是最高字节(网络字节序),10.20.30.40 会变成 11.20.30.40,不是 …41。
2.3 TCP(流式)服务
服务器:socket → bind → listen → accept → recv/send → close
客户端:socket → connect → send/recv → close
/* 服务器:收到整数 +1 后返回 */
sock = socket(AF_INET, SOCK_STREAM, 0);
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(PORT);
my_addr.sin_addr.s_addr = INADDR_ANY;
bind(sock, (struct sockaddr *)&my_addr, sizeof(my_addr));
listen(sock, 5);
while (1) {
new_sock = accept(sock, (struct sockaddr *)&client_addr, &len);
printf("get connection from %s, port %d socket %d\\n",
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), new_sock);
len = recv(new_sock, buf, 100, 0);
if (len == 0) { printf("the client quit!\\n"); break; } // 对端关闭
buf[len] = '\\0';
recdata = atoi(buf) + 1;
sprintf(buf2, "%d", recdata);
send(new_sock, buf2, strlen(buf2), 0);
close(new_sock);
}
/* 客户端 */
sock = socket(AF_INET, SOCK_STREAM, 0);
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(8888);
inet_aton(argv[1], (struct in_addr *)&my_addr.sin_addr.s_addr);
connect(sock, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));
send(sock, buf, strlen(buf) – 1, 0);
recv(sock, recbuf, 100, 0);
2.4 UDP(数据报)服务
服务器:socket → bind → recvfrom/sendto → close (无 listen、无 accept)
客户端:socket → sendto/recvfrom → close (无 connect)
sock = socket(AF_INET, SOCK_DGRAM, 0);
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(8888);
s_addr.sin_addr.s_addr = INADDR_ANY;
bind(sock, (struct sockaddr *)&s_addr, sizeof(s_addr));
len = recvfrom(sock, buf, sizeof(buf) – 1, 0, (struct sockaddr *)&c_addr, &addr_len);
buf[len] = '\\0';
sendto(sock, buf2, strlen(buf2), 0, (struct sockaddr *)&c_addr, addr_len);
/* 客户端:./udpclient 127.0.0.1 1234 */
sock = socket(AF_INET, SOCK_DGRAM, 0);
s_addr.sin_addr.s_addr = inet_addr(argv[1]);
strcpy(buff, argv[2]);
sendto(sock, buff, strlen(buff), 0, (struct sockaddr *)&s_addr, addr_len);
recvfrom(sock, buff, sizeof(buff) – 1, 0, (struct sockaddr *)&s_addr, &addr_len);
2.5 原始套接字
可自己构造协议头部、监听网络报文(普通 socket 只能把数据装进 TCP/UDP 头里)。ping、traceroute、tcpdump 都基于它。⚠ 需 root 权限。
int sockfd = socket(domain, SOCK_RAW, protocol); // 第 3 个参数通常不为 0
链路层:socket(PF_PACKET, type, htons(protocol))
| ETH_P_ALL (0x0003) | 收到所有二层报文 |
| ETH_P_IP (0x0800) | 只收目的为本机 IP 的报文(已剥链路层头) |
| ETH_P_ARP / ETH_P_RARP | ARP / RARP 报文 |
| 不指定 | 只能发不能收 |
- SOCK_RAW:连链路层头都要自己构造;SOCK_DGRAM:内核帮你剥/填链路层头。
网络层:socket(PF_INET, SOCK_RAW, protocol)
| IPPROTO_TCP / UDP / ICMP / IGMP | 收对应类型报文 |
| IPPROTO_RAW (255) | 只能发,需自己构造 IP 首部 |
- 收到的报文从 IP 首部开始(已剥链路层);发送时自行构造 TCP/UDP 头,设 IP_HDRINCL 后可自行构造 IP 头。
- ⚠ TCP/UDP 报文不会交给网络层原始套接字。
sock = socket(AF_INET, SOCK_RAW, IPPROTO_TCP); // 监听 TCP 报文
Set_Promisc("eth0", sock); // ioctl + IFF_PROMISC 设混杂模式
n = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr *)&from, &fromlen);
ip = (struct ip *)buffer; // IP 头
tcp = (struct tcp *)(buffer + 20); // IP 头 20 字节后是 TCP 头
printf("%s → %s ttl=%d proto=%d port %d→%d\\n", ...);
三. 高级 I/O
3.1 阻塞与非阻塞
- 阻塞(默认):read/write/send/recv/connect/accept 都会等到有结果才返回。
- 非阻塞:调用立即返回,没数据/没连接时返回错误(通常是 EWOULDBLOCK)。
/* fcntl 方式 */
flag = fcntl(fd, F_GETFL, 0);
flag |= O_NONBLOCK;
fcntl(fd, F_SETFL, flag);
/* ioctl 方式 */
int b_on = 1;
ioctl(fd, FIONBIO, &b_on); // 1 设置非阻塞,0 清除
非阻塞下各调用的返回值处理
| connect | 返回 -1 且 errno == EINPROGRESS 表示握手进行中,用 select 检测完成 |
| 发送 | 缓冲区无空间返回 EWOULDBLOCK;空间不足则发多少算多少,循环续发 |
| 接收 | retval > 0:等于请求长度说明没读完;< 0:EWOULDBLOCK 重试、EINTR 重试;== 0 对端已关闭 |
| accept | 无连接时返回 EWOULDBLOCK,继续 accept |
/* 非阻塞发送骨架 */
int nLeft = nLen, write_pos = 0;
while (nLeft > 0) {
int nwrite = write(sock_fd, buf + write_pos, nLeft);
if (nwrite < 0) {
if (errno == EWOULDBLOCK) nwrite = 0; // 重发
else return –1;
}
nLeft -= nwrite; write_pos += nwrite;
}
3.2 select 多路复用
一个进程内同时监视多个描述符,谁就绪就处理谁(适合多描述符,单个描述符不如直接阻塞)。
int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
| numfds | 监视的最大描述符 + 1 |
| timeout | NULL 一直等;具体值超时返回;0 立即返回 |
| 返回值 | 就绪的描述符数;0 超时;-1 出错 |
FD_ZERO(&set); FD_SET(fd, &set); FD_CLR(fd, &set); FD_ISSET(fd, &set);
单进程并发服务器骨架
while (1) {
FD_ZERO(&fdsr);
FD_SET(sock_fd, &fdsr);
for (i = 0; i < BACKLOG; i++) if (fd_A[i]) FD_SET(fd_A[i], &fdsr);
ret = select(maxsock + 1, &fdsr, NULL, NULL, &tv);
if (ret == 0) { printf("timeout\\n"); continue; }
for (i = 0; i < conn_amount; i++) // 已有连接有数据
if (FD_ISSET(fd_A[i], &fdsr)) {
ret = recv(fd_A[i], buf, sizeof(buf), 0);
if (ret <= 0) { close(fd_A[i]); FD_CLR(fd_A[i], &fdsr); fd_A[i] = 0; }
else printf("client[%d] send:%s\\n", i, buf);
}
if (FD_ISSET(sock_fd, &fdsr)) { // 新连接
new_fd = accept(sock_fd, (struct sockaddr *)&client_addr, &sin_size);
fd_A[conn_amount++] = new_fd;
if (new_fd > maxsock) maxsock = new_fd;
}
}
⚠ 客户端频繁断开/连接会让 maxfd 越来越大,应在每次 accept 后重新扫描已有 fd 求最大值。
3.3 poll
int poll(struct pollfd *fds, int numfds, int timeout); // timeout 单位毫秒,≤0 无限等待
struct pollfd { int fd; short events; short revents; };
| POLLIN | 有数据可读 |
| POLLPRI | 有紧急数据可读 |
| POLLOUT | 可写 |
| POLLERR / POLLHUP / POLLNVAL | 出错 / 断开 / 描述符非法(仅输出) |
clients[0].fd = listenfd; clients[0].events = POLLIN;
for (i = 1; i < OPEN_MAX; i++) clients[i].fd = –1;
nready = poll(clients, maxi + 1, –1);
if (clients[0].revents & POLLIN) { connfd = accept(...); /* 找一个 fd == -1 的槽位放入 */ }
for (i = 1; i < maxclient; i++)
if (clients[i].revents & (POLLIN | POLLERR)) {
nread = read(clients[i].fd, buf, MAXLINE);
if (nread <= 0) { close(...); clients[i].fd = –1; }
}
3.4 epoll
事件通知机制:系统直接告诉用户哪些 socket 活跃,不用轮询全部。
| 连接上限 | select 一般 2048 | 无硬限制(受最大打开文件数限制) |
| 效率 | 遍历整个集合,越大越慢 | 只关心活跃连接,与总数无关 |
| 数据传递 | 内核 → 用户拷贝 | 共享内存 |
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // ADD / MOD / DEL
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
struct epoll_event { uint32_t events; epoll_data_t data; };
typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t;
| EPOLLIN / EPOLLOUT | 可读 / 可写 |
| EPOLLERR / EPOLLHUP | 出错 / 挂断 |
| EPOLLET | 边缘触发 |
listenfd = socket(AF_INET, SOCK_STREAM, 0);
setnonblocking(listenfd); // fcntl 加 O_NONBLOCK
bind(...); listen(...);
epollfd = epoll_create(EPOLLSIZE);
ev.events = EPOLLIN | EPOLLET; ev.data.fd = listenfd;
epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &ev);
for (;;) {
nfds = epoll_wait(epollfd, events, curfds, –1);
for (n = 0; n < nfds; n++) {
if (events[n].data.fd == listenfd) { // 新连接
connfd = accept(listenfd, ...);
setnonblocking(connfd);
ev.events = EPOLLIN | EPOLLET; ev.data.fd = connfd;
epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &ev);
} else { // 客户端数据
if (handle(events[n].data.fd) < 0) {
epoll_ctl(epollfd, EPOLL_CTL_DEL, events[n].data.fd, &ev);
close(events[n].data.fd);
}
}
}
}
实践建议:监视线程只做监视,事件交给线程池处理,避免每次事件来才建线程。
四. 并发服务器
4.1 多进程模型
父进程:socket → bind → listen → accept → fork → 继续 accept
子进程:close(listenfd) → 处理该客户端 → close(connfd)
- 子进程处理一个客户的同时,父进程可继续接收下一个请求 → 提高并发。
- ⚠ 子进程再 fork 一个孙子进程干活,子进程直接退出:父进程不必等孙子,天然避免僵尸进程。
while (1) {
newconn_fd = accept(sock_fd, NULL, NULL);
pid = fork();
if (pid == 0) { // 子进程
ppid = fork();
if (ppid == 0) { // 孙子进程真正干活
handle_conn(newconn_fd);
close(sock_fd);
}
}
}
4.2 三种并发方式对比
| 多进程/多线程 | 模型简单、并发性好 | 需处理同步互斥;阻塞等待 |
| select/poll | 单进程服务多连接,无同步互斥问题 | 要不断轮询,描述符越多越慢 |
| epoll | 事件通知,只处理活跃连接,适合大量连接 | Linux 特有 |
网硕互联帮助中心




评论前必须登录!
注册