基于 UNIX Network Programming, Volume 1: The Sockets Networking API, Third Edition 编写
套接字介绍
IPv4 套接字地址结构:
struct in_addr { in_addr_t s_addr; }
struct sockaddr_in { uint8_t sin_len; sa_family_t sin_family; in_port_t sin_port; struct in_addr sin_addr; char sin_zero[8]; }
|
一般只需要设置中间三个成员即可
有更广泛的 socket 地址结构:
struct sockaddr { uint8_t sa_len; sa_family_t sa_family; char sa_data[14]; }
|
因此,在诸如 bind
的函数中,都会通过 (struct sockaddr *)
转换
IPv6 套接字地址结构:
struct in6_addr { uint8_t s6_addr[16]; }
struct sockaddr_in6 { uint8_t sin6_len; sa_family_t sin6_family; in_port_t sin6_port; struct in6_addr sin6_addr; }
|
因为有 IPv6 的加入,更广泛的 socket 地址结构也有了改变:
struct sockaddr_storage { uint8_t ss_len; sa_family_t ss_family; }
|
对于 connect
,socket 地址从进程送到内核中
![从进程到内核](data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)
对于 getpeername
,socket 地址会从内核传递到进程中
![从内核到进程](data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)
网络中的字节序和本机的字节序可能不同,可以通过 htons
(主机到网络 16 bit),htonl
(主机到网络 32 bit),ntohs
(网络到主机 16 bit),ntohl
(网络到主机 32 bit)
一般使用 memset
将 socket 地址结构初始化为 0
inet_pton
可以将 IPv4 或 IPv6 地址字符串转换为 IP 地址结构中的数据,p
代表 presentation,n
代表 numeric
也有相反功能的 inet_ntop
基本 TCP socket
![TCP客户端和服务端](data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)
使用 socket
函数产生一个 socket
connect
函数用于客户端向服务端建立连接
bind
函数将协议地址赋值给 socket
表示任意地址的方法:
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); serv.sin6_addr = in6addr_any;
|
listen
是服务端调用,执行向内核指示将会接收连接请求,第二个参数指明了内核应该排队的 socket 的最大数量
![对一个正在 listen 的 socket 的维护的两个队列](data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)
accept
返回下一个完成连接的队列的连接
fork
函数可以建立新进程(调用一次,返回两次)
exec
函数用于执行新的程序(调用一次,不会返回)
记得使用 close
函数关闭 socket 并终止 TCP 连接
getsockname
和 getpeername
函数可以获取 socket 的本地(getsockname)或外部(getpeername)地址
TCP 客户端/服务端例子
tcpserv.c#include <arpa/inet.h> #include <errno.h> #include <signal.h> #include <stdio.h> #include <string.h> #include <strings.h> #include <sys/socket.h> #include <sys/wait.h> #include <unistd.h>
void str_echo(int sockfd) { char buffer[1024]; while (1) { ssize_t n = recv(sockfd, buffer, sizeof(buffer), 0); if (n > 0) { buffer[n] = '\0'; printf("recv: %s", buffer); send(sockfd, buffer, strlen(buffer), 0); } else if (n < 0 && errno != EINTR) { perror("str_echo: recv error"); return; } } }
void sig_chld(int signo) { pid_t pid; int stat; while ((pid = waitpid(-1, &stat, WNOHANG)) > 0) { printf("child %d terminated\n", pid); } return; }
int main() { int listenfd = socket(AF_INET, SOCK_STREAM, 0); if (listenfd < 0) { perror("socket 建立失败"); return 1; } struct sockaddr_in servaddr; bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(8888);
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) { perror("bind 失败"); return 1; }
if (listen(listenfd, 5) < 0) { perror("listen 失败"); return 1; }
signal(SIGCHLD, sig_chld);
for (;;) { struct sockaddr_in cliaddr; socklen_t clilen = sizeof(cliaddr); int connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen); if (connfd < 0) { if (errno == EINTR) { continue; } else { perror("accept 失败"); return 1; } } if (fork() == 0) { close(listenfd); str_echo(connfd); close(connfd); return 0; } close(connfd); } return 0; }
|
tcpcli.c#include <arpa/inet.h> #include <stdio.h> #include <string.h> #include <strings.h> #include <sys/socket.h> #include <unistd.h>
void str_cli(FILE *fp, int sockfd) { char sendline[1024], recvline[1024]; while (fgets(sendline, sizeof(sendline), fp) != NULL) { write(sockfd, sendline, strlen(sendline)); int n; if ((n = read(sockfd, recvline, sizeof(recvline))) == 0) { perror("str_cli: server terminated prematurely"); return; } recvline[n] = '\0'; fputs(recvline, stdout); } }
int main() { int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { perror("socket 失败"); return 1; }
struct sockaddr_in servaddr; bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(8888); inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) { perror("connect 失败"); return 1; } str_cli(stdin, sockfd); return 0; }
|
tcpserv.c#include <WinSock2.h> #include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
void str_echo(SOCKET sockfd) { char buffer[1024]; while (1) { int n = recv(sockfd, buffer, sizeof(buffer) - 1, 0); if (n > 0) { buffer[n] = '\0'; printf("recv: %s", buffer); send(sockfd, buffer, strlen(buffer), 0); } } }
int main() { WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { return 1; }
SOCKET listenfd = socket(AF_INET, SOCK_STREAM, 0); if (listenfd == INVALID_SOCKET) { perror("socket 失败"); return 1; } struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(8888); servaddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY); if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == SOCKET_ERROR) { perror("socket 失败"); return 1; }
if (listen(listenfd, 5) == SOCKET_ERROR) { perror("listen 失败"); return 1; }
while (1) { struct sockaddr_in cliaddr; int len = sizeof(cliaddr); SOCKET connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &len); if (connfd == INVALID_SOCKET) { if (errno == EINTR) { continue; } else { perror("accept 失败"); return 1; } } CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)str_echo, (LPVOID)connfd, 0, NULL); }
closesocket(listenfd); WSACleanup(); return 0; }
|
tcpcli.c#include <WS2tcpip.h> #include <Winsock2.h> #include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
void str_cli(FILE* fp, SOCKET sockfd) { char sendline[1024], recvline[1024]; while (fgets(sendline, sizeof(sendline), fp) != NULL) { send(sockfd, sendline, strlen(sendline), 0); int n; if ((n = recv(sockfd, recvline, sizeof(recvline) - 1, 0)) == 0) { printf("str_cli: server terminated prematurely\n"); return; } recvline[n] = '\0'; fputs(recvline, stdout); } }
int main() { WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { printf("WSAStartup failed.\n"); return 1; } SOCKET sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (sockfd == INVALID_SOCKET) { printf("socket failed.\n"); return 1; }
struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(8888); inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
if (connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == SOCKET_ERROR) { printf("connect failed.\n"); return 1; }
str_cli(stdin, sockfd);
WSACleanup(); return 0; }
|
I/O 复用:select
和 poll
函数
有以下五种 I/O 模型:
![阻塞IO模型](data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)
![非阻塞IO模型](data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)
![IO多路复用模型](data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)
![信号驱动IO模型](data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)
![异步IO模型](data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)
select
函数运行内核等待多个事件中的一个发生,且当多个事件中的一个或多个发生,或超时时,唤醒进程:
int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
|
通常使用 FD_ZERO
、FD_SET
等函数来向 fd_set
中添加或删除元素
pselect
函数类似,但改变了 timespec
结构和禁用某些信号
poll
函数类似于 select
,但在处理 STREAMS 设备时提供了额外信息
int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);
|
#include <arpa/inet.h> #include <errno.h> #include <poll.h> #include <stdio.h> #include <string.h> #include <strings.h> #include <sys/poll.h> #include <sys/socket.h> #include <sys/wait.h> #include <unistd.h>
#define OPEN_MAX 1024 #define MAXLINE 1024
int main() { int listenfd = socket(AF_INET, SOCK_STREAM, 0); if (listenfd < 0) { perror("socket 建立失败"); return 1; } struct sockaddr_in servaddr; bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(8888);
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) { perror("bind 失败"); return 1; }
if (listen(listenfd, 5) < 0) { perror("listen 失败"); return 1; }
struct pollfd client[OPEN_MAX]; client[0].fd = listenfd; client[0].events = POLLRDNORM; for (int i = 1; i < OPEN_MAX; i++) { client[i].fd = -1; } int maxi = 0;
char buf[MAXLINE];
for (;;) { int nready = poll(client, maxi + 1, -1); if (client[0].revents & POLLRDNORM) { int connfd = accept(listenfd, NULL, NULL); if (connfd < 0) { perror("accept 失败"); return 1; } int i; for (i = 1; i < OPEN_MAX; i++) { if (client[i].fd < 0) { client[i].fd = connfd; client[i].events = POLLRDNORM; break; } } if (i == OPEN_MAX) { perror("too many clients"); return 1; } if (i > maxi) { maxi = i; } if (--nready <= 0) { continue; } }
for (int i = 1; i <= maxi; i++) { int sockfd = client[i].fd; if (sockfd < 0) { continue; } if (client[i].revents & (POLLRDNORM | POLLERR)) { int n; if ((n = read(sockfd, buf, MAXLINE)) < 0) { if (errno == ECONNRESET) { close(sockfd); client[i].fd = -1; } else { perror("str_echo: read error"); return 1; } } else if (n == 0) { close(sockfd); client[i].fd = -1; } else { write(sockfd, buf, n); } if (--nready <= 0) { break; } } } } return 0; }
|
#include <arpa/inet.h> #include <stdio.h> #include <string.h> #include <strings.h> #include <sys/select.h> #include <sys/socket.h> #include <unistd.h>
#define MAXLINE 1024
void str_cli(FILE *fp, int sockfd) { char buf[MAXLINE]; int maxfdp1, stdineof; int n; fd_set rset; FD_ZERO(&rset); stdineof = 0;
for (;;) { if (stdineof == 0) FD_SET(fileno(fp), &rset); FD_SET(sockfd, &rset); maxfdp1 = (fileno(fp) > sockfd ? fileno(fp) : sockfd) + 1; select(maxfdp1, &rset, NULL, NULL, NULL)); if (FD_ISSET(sockfd, &rset)) { if ((n = read(sockfd, buf, MAXLINE)) == 0) { if (stdineof == 1) { return; } else { perror("str_cli: server terminated prematurely"); return; } } write(fileno(stdout), buf, n); } if (FD_ISSET(fileno(fp), &rset)) { if ((n = read(fileno(fp), buf, MAXLINE)) == 0) { stdineof = 1; shutdown(sockfd, SHUT_WR); FD_CLR(fileno(fp), &rset); continue; } write(sockfd, buf, n); } } }
int main() { int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { perror("socket 失败"); return 1; }
struct sockaddr_in servaddr; bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(8888); inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) { perror("connect 失败"); return 1; } str_cli(stdin, sockfd); return 0; }
|
基本 UDP socket
![UDP服务器和客户端](data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)
recvfrom
和 sendto
类似于 read
和 write
,但是多出目标的 socket 地址
#include <arpa/inet.h> #include <stdio.h> #include <strings.h> #include <sys/socket.h>
#define MAXLINE 1000
void dg_echo(int sockfd, struct sockaddr *pcliaddr, socklen_t clilen) { char mesg[MAXLINE]; for (;;) { socklen_t len = clilen; int n = recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &len); sendto(sockfd, mesg, n, 0, pcliaddr, len); } } int main() { struct sockaddr_in servaddr, cliaddr; int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd < 0) { perror("socket failed"); return 0; }
bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(8080); if (bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) { perror("bind failed"); return 0; } dg_echo(sockfd, (struct sockaddr *)&cliaddr, sizeof(cliaddr)); return 0; }
|
#include <arpa/inet.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <strings.h> #include <sys/socket.h>
#define MAXLINE 1000 void dg_cli(FILE *fp, int sockfd, struct sockaddr *pservaddr, socklen_t servlen) { char sendline[MAXLINE], recvline[MAXLINE + 1]; struct sockaddr *preply_addr; preply_addr = malloc(servlen); while (fgets(sendline, MAXLINE, fp) != NULL) { sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen); socklen_t len = servlen; int n = recvfrom(sockfd, recvline, MAXLINE, 0, preply_addr, &len); if (len != servlen || memcmp(pservaddr, preply_addr, len) != 0) { printf("reply from %s (ignored)\n", inet_ntoa(((struct sockaddr_in *)preply_addr)->sin_addr)); continue; } recvline[n] = 0; fputs(recvline, stdout); } }
int main() { int sockfd; struct sockaddr_in servaddr; sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd < 0) { perror("socket failed"); return 0; } bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(8080); inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
dg_cli(stdin, sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); return 0; }
|
connect
同样适用于 UDP,但并不会建立连接,而是记录下同伴的 IP 地址和端口号
UDP 可能会丢包,也没有流控制
可以将 TCP 和 UDP 结合起来,同时支持两种协议的请求
名字和地址转换
getaddrinfo
函数既可以从名字到地址,也可以从服务到端口:
int getaddrinfo(const char *hostname, const char *service, const struct addrinfo *hints, struct addinfo **result);
|
其中 addrinfo
结构如下:
struct addrinfo { int ai_flags; int ai_family; int ai_socktype; int ai_protocol; socklen_t ai_addrlen; char *ai_canonname; struct sockaddr *ai_addr; struct addrinfo *ai_next; }
|
hints
中可以设置前四个
通过 getaddrinfo
获取的链表需要用户手动释放 freeaddrinfo
一个使用域名和服务名而不是 IP 地址和端口号与服务器建立连接的函数:
int tcp_connect(const char *host, const char *service) { struct addrinfo hints; bzero(&hints, sizeof(hints)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; struct addrinfo *res, *ressave; if (getaddrinfo(host, service, &hints, &res) != 0) { return -1; } ressave = res; int sockfd; do { sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); if (sockfd < 0) { continue; } if (connect(sockfd, res->ai_addr, res->ai_addrlen) == 0) { return sockfd; } close(sockfd); } while ((res = res->ai_next) != NULL); if (res == NULL) { return -1; } freeaddrinfo(ressave); return sockfd; }
|
对于服务端,我们可以使用这种方案做到协议无关(IPv4 + IPv6):
int tcp_listen(const char *host, const char *serv, socklen_t *addrlenp) { struct addrinfo hints, *res, *ressave; bzero(&hints, sizeof(hints)); hints.ai_flags = AI_PASSIVE; hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; if (getaddrinfo(host, serv, &hints, &res) != 0) { return -1; } ressave = res; int listenfd; const int on = 1; do { listenfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); if (listenfd < 0) { continue; } setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); if (bind(listenfd, res->ai_addr, res->ai_addrlen) == 0) { break; } close(listenfd); } while ((res = res->ai_next) != NULL); if (res == NULL) { return -1; } listen(listenfd, 1024); if (addrlenp) { *addrlenp = res->ai_addrlen; } freeaddrinfo(ressave); return listenfd; }
|
UDP 同理,但是 hints.ai_socktype = SOCK_DGRAM
IPv4 和 IPv6 互操作性
IPv4 客户端,IPv6 服务端:
- 客户端能够找到服务器的 A 记录,创建 IPv4 socket
- 服务器接收时将 IPv4 映射为 IPv6,然后就是正常处理 IPv6
- 响应会生成 IPv4 包
即两者都认为自己在对等通信
IPv6 客户端,IPv4 服务端同理
Daemon 进程和超服务器
因为 deamon 程序没有控制台,故可以向 syslogd
发送日志信息:
void syslog(int priority, const char *message, ...);
|
由于一般会有多个服务器存在,故 Unix 使用一个进程 inetd
统一管理客户端请求并启动相应的服务进程
![inetd](data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)