云计算百科
云计算领域专业知识百科平台

30天速通Linux 第九章Linux网络编程

一. 计算机网络概述

1.1 TCP/IP 五层模型

层名称要点
5 应用层 应用程序使用互联网的规程
4 传输层 TCP/UDP;进程到进程的通信、流量控制、差错控制
3 互联网层 IP;不可靠、无连接、尽最大努力交付;32 位 IP 地址
2 数据链路层 组帧与传帧(帧是传输单位)
1 物理层 硬件设备的电气特性规范

收发流程:发送时自上而下 插口层 → 协议层(加 TCP/UDP 头 → 加 IP 头)→ 接口层(加以太网头)→ 网卡;接收时反向逐层剥离。

1.2 TCP vs UDP

TCPUDP
连接 面向连接(三次握手) 无连接
可靠性 可靠(确认 + 超时重传 + 滑动窗口流控) 不可靠,无流控无确认
适用 一般数据传输 简单请求-响应、网络条件好
效率 有连接开销 快,但差错/流控要应用自己做
  • 用端口(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))

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 报文不会交给网络层原始套接字。

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; };

events含义
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/pollepoll
连接上限 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;

events含义
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 特有
赞(0)
未经允许不得转载:网硕互联帮助中心 » 30天速通Linux 第九章Linux网络编程
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!