一. 计算机网络概述
1.1 TCP/IP 五层模型
| 层 | 名称 | 要点 |
|---|---|---|
| 5 | 应用层 | 应用程序使用互联网的规程 |
| 4 | 传输层 | TCP/UDP;进程到进程的通信、流量控制、差错控制 |
| 3 | 互联网层 | IP;不可靠、无连接、尽最大努力交付;32 位 IP 地址 |
| 2 | 数据链路层 | 组帧与传帧(帧是传输单位) |
| 1 | 物理层 | 硬件设备的电气特性规范 |
收发流程:发送时自上而下 插口层 → 协议层(加 TCP/UDP 头 → 加 IP 头)→ 接口层(加以太网头)→ 网卡;接收时反向逐层剥离。
1.2 TCP vs UDP
| TCP | 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
c
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 + 地址结构
c
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
c
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。
收发
c
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 字节序与地址转换
- 网络字节序 = 大端;主机可能小端,必须转换。
c
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
c
/* 服务器:收到整数 +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);
}
c
/* 客户端 */
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)
c
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);
c
/* 客户端:./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 权限。
c
int sockfd = socket(domain, SOCK_RAW, protocol); // 第 3 个参数通常不为 0
链路层 :socket(PF_PACKET, type, htons(protocol))
| 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)
| protocol | 作用 |
|---|---|
IPPROTO_TCP / UDP / ICMP / IGMP |
收对应类型报文 |
IPPROTO_RAW (255) |
只能发,需自己构造 IP 首部 |
- 收到的报文从 IP 首部开始 (已剥链路层);发送时自行构造 TCP/UDP 头,设
IP_HDRINCL后可自行构造 IP 头。 - ⚠ TCP/UDP 报文不会交给网络层原始套接字。
c
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)。
c
/* 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 |
c
/* 非阻塞发送骨架 */
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 多路复用
一个进程内同时监视多个描述符,谁就绪就处理谁(适合多描述符,单个描述符不如直接阻塞)。
c
int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
| 参数 | 说明 |
|---|---|
numfds |
监视的最大描述符 + 1 |
timeout |
NULL 一直等;具体值超时返回;0 立即返回 |
| 返回值 | 就绪的描述符数;0 超时;-1 出错 |
c
FD_ZERO(&set); FD_SET(fd, &set); FD_CLR(fd, &set); FD_ISSET(fd, &set);
单进程并发服务器骨架
c
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
c
int poll(struct pollfd *fds, int numfds, int timeout); // timeout 单位毫秒,≤0 无限等待
struct pollfd { int fd; short events; short revents; };
| events | 含义 |
|---|---|
POLLIN |
有数据可读 |
POLLPRI |
有紧急数据可读 |
POLLOUT |
可写 |
POLLERR / POLLHUP / POLLNVAL |
出错 / 断开 / 描述符非法(仅输出) |
c
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/poll | epoll |
|---|---|---|
| 连接上限 | select 一般 2048 | 无硬限制(受最大打开文件数限制) |
| 效率 | 遍历整个集合,越大越慢 | 只关心活跃连接,与总数无关 |
| 数据传递 | 内核 → 用户拷贝 | 共享内存 |
c
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);
c
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;
| events | 含义 |
|---|---|
EPOLLIN / EPOLLOUT |
可读 / 可写 |
EPOLLERR / EPOLLHUP |
出错 / 挂断 |
EPOLLET |
边缘触发 |
c
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一个孙子进程干活,子进程直接退出:父进程不必等孙子,天然避免僵尸进程。
c
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 特有 |