1. 套接字设置
基本的网络应用模型如图所示:
客户与服务器通过套接字进行通信。套接字就像一个文件描述符,可以通过它进行读写。
==客户和服务器连接首先要设置套接字==
客户与服务器建立 TCP 连接过程:
1.1 客户端套接字设置
- socket
- connect
基本 TCP 客户/服务器程序的套接字函数
int sockfd;
struct sockaddr_in servaddr;
if (argc != 2)
err_quit("usage: tcpcli <IPaddress>");
// 创建套接字
sockfd = Socket(AF_INET, SOCK_STREAM, 0);
// 网络连接
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
Connect(sockfd, (SA *) &servaddr, sizeof(servaddr));
- 7: 创建套接字,返回一个描述符,用于通信
- 9~15 行: 创建连接,主要是 connect 函数。客户端调用 Connect 函数,==用于指定要连接服务端的 IP 地址和端口号==
客户在调用函数connect前不必非得调用bind函数(我们在下一节介绍该函数),因为如果需要的话,内核会确定源IP地址,并选择一个临时端口作为源端口。
- 调用 Connect 函数将会激发三次握手1
1.2 服务端套接字设置
服务器的套接字设置分为三个步骤
- socket
- bind
- listen
- accept
int listenfd;
struct sockaddr_in cliaddr, servaddr;
// 第一步:socket
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
// 第二部:bind
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
// 第三步:listen
Listen(listenfd, LISTENQ);
for(;;){
clilen = sizeof(cliaddr);
// 第四步:accept
connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);
// 网络处理函数
************
}
Socket 函数(第 7 行)
- 表示服务端创建了一个套接字,这是必须的步骤
Bind 函数(9~14 行)
- 用于指定一个端口号与 IP 地址,可以都指定,也可以都不指定
- 对于服务器来讲,指定的 IP 地址表示:该套接字只接受那些目的地为这个 IP 地址的客户连接
- 对于服务器来讲,指定的端口号表示:服务器采用哪个端口号服务客户端(一般来讲必须指定)
注意: 对于客户,不一定需要 Bind,到时候系统可以自动设置。客户套接字主要需要设置服务器的 IP 和端口号。
Listen 函数(14 行)
当socket函数创建一个套接字时,它被假设为一个主动套接字,也就是说,它是一个将调用connect发起连接的客户套接字。listen函数把一个未连接的套接字转换成一个被动套接字,指示内核应接受指向该套接字的连接请求。
- 本函数通常应该在调用 socket 和 bind 这两个函数之后,并在调用 accept 函数之前调用。
- 第二个参数规定了内核应该为相应套接字排队的最大连接个数。
第二个参数的理解
accept 函数(19 行)
-
如果 accept 成功,那么其返回值是由内核自动生成的一个全新描述符,代表与所返回客户
的 TCP 连接。
-
一个服务器通常仅仅创建一个监听套接字,它在该服务器的生命期内一直存在。
-
内核为每个由服务器进程接受的客户连接创建一个已连接套接字(也就是说对于它的 TCP 三路握手过程已经完成)。当服务器完成对某个给定客户的服务时,相应的已连接套接字就被关闭。
TCP 客户/服务器程序
我们要编写一个完整的 TCP 客户/服务器程序示例,如图所示
- 客户从标准输入接收数据,写给服务器
- 服务器从网络读入这行文本,并回射给客户
- 客户从网络读入这行文本,并写给标准输出
此外,我们还要讨论它的边界条件:
- 正常启动的情况
- 客户正常终止发生什么
- 服务器在客户之前终止会发生什么
1. 服务器主函数
结构:
- 服务器套接字设置:1.2 服务端套接字设置2
- 回射函数(服务器从网络读入文本,并回射给客户)
使用了高并发技术
#include "unp.h"
int
main(int argc, char **argv)
{
// 第一部分:服务器套接字设置
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
// 高并发编程
for ( ; ; ) {
clilen = sizeof(cliaddr);
connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);
// 第二部分:回射函数
if ( (childpid = Fork()) == 0) { /* child process */
Close(listenfd); /* close listening socket */
str_echo(connfd); /* process the request */
exit(0);
}
Close(connfd); /* parent closes connected socket */
}
}
代码解释3:
-
7~21 行:套接字设置,设置完成后进入循环
-
26 行: 服务器阻塞于 Accept 调用,直到有客户完成连接。客户连接后,程序继续向下执行。
-
29 行: 并发编程
- 我们并不希望整个服务器被单个客户长期占用,而是希望同时服务多个客户。
- 在子进程中,关闭 listenfd;在父进程中,关闭,connfd。因为在各自的进程中不使用它们
-
34 行 str_echo: 在子进程中,用过回射函数处理客户请求
TCP 回射服务器程序:str_echo 函数
- 从网络连接读取数据
- 向客户端回射
str_echo.c
#include "unp.h"
void
str_echo(int sockfd)
{
ssize_t n;
char buf[MAXLINE];
again:
while ( (n = read(sockfd, buf, MAXLINE)) > 0){
Writen(sockfd, buf, n);
// 向标准输出输出buf
Writen(STDOUT_FILENO, buf, n);
}
if (n < 0 && errno == EINTR)
goto again;
else if (n < 0)
err_sys("str_echo: read error");
}
- 10 ~14:服务器从网络连接中读数据。然后向客户端写回(11 行)
- 第 10 行中 read 函数没有用包裹函数,因为我们需要单独处理 read 的错误(信号中断)
- 16 行: 如果遇到信号中断,read 函数返回并且将 errno 设为 EINTR。之后程序会跳转到 again 重新执行
- 如果只是正常退出,比如客户端发送 FIN。那么 read 函数会读到 EOF,直接返回,并且 n<0,则退出 str_echo 函数
2. 客户端主函数
- 客户从标准输入接收数据,写给服务器
结构:
- 客户端套接字设置:1.1 客户端套接字设置4
- 回射函数(服务器从网络读入文本,并回射给客户)
#include "unp.h"
int
main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in servaddr;
if (argc != 2)
err_quit("usage: tcpcli <IPaddress>");
sockfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
Connect(sockfd, (SA *) &servaddr, sizeof(servaddr));
str_cli(stdin, sockfd); /* do it all */
exit(0);
}
代码解释5
创建套接字,装填网际网套接字地址结构
- 9~13 创建一个 TCP 套接字,用服务器的 IP 地址和端口号装填一个网际网套接字地址结构。我们可从命令行参数取得服务器的 IP 地址,从头文件 unp.h 取得服务器的众所周知端口号(SERV_PORT)。
连接到套接字
- 14~15 connect 建立与服务器的连接。str_cli 函数完成剩余部分的客户处理工作
客户发射函数
#include "unp.h"
void
str_cli(FILE *fp, int sockfd)
{
char sendline[MAXLINE], recvline[MAXLINE];
while (Fgets(sendline, MAXLINE, fp) != NULL) {
Writen(sockfd, sendline, strlen(sendline));
if (Readline(sockfd, recvline, MAXLINE) == 0)
err_quit("str_cli: server terminated prematurely");
Fputs(recvline, stdout);
}
}
读入一行,写到服务器
- 6~7 fgets 读入一行文本,writen 把该行发送给服务器。
从服务器读入回射行,写到标准输出
- 8~10 readline 从服务器读入回射行,fputs 把它写到标准输出。
返回到 main 函数
- 11~12 当遇到文件结束符或错误时,fgets 将返回一个空指针,于是客户处理循环终止。我们的 Fgets 包裹函数检查是否发生错误,若发生则中止进程,因此 Fgets 只是在遇到文件结束符时才返回一个空指针。
3. 正常启动
查看端口号
TCP 服务器正常启动后,若是由内核自动分配的端口,可通过 getsockname 来获取端口号
** // 获取已分配的端口**
getsockname(listenfd, (struct sockaddr*)&servaddr, &addr_len);
printf("Port assigned by the system: %d\n", ntohs(servaddr.sin_port));
查看 TCP 状态 1
然后启动客户端连接,在服务器端可以查看连接状态,通过 netstat -a | grep 端口号
wzh@xjc-PowerEdge-R740:~$ netstat -a | grep 9877
tcp 0 0 0.0.0.0:9877 0.0.0.0:* LISTEN
tcp 0 0 3448edf3d71a:9877 387a0ea8a205:64082 ESTABLISHED
查看 TCP 状态 2
也可以通过 ps -ef | grep 程序名称
查看服务程序,可以看到虚拟终端编号
我们看到虚拟终端在 pts/2 处。可以通过
ps -t ==虚拟终端号== -o pid,ppid,tty,stat,args,wchan,查看服务程序的具体状态:
4. 正常终止
-
客户端按 Ctrl + D,即读取到 EOF 字符,fgets 返回一个空指针,于是 str_cli.c6函数返回。
-
当 str_cli 返回到客户的 main 函数5时,main 通过调用 exit 终止。
-
这导致客户 TCP 发送一个 FIN 给服务器,服务器 TCP 则以 ACK 响应,这就是 TCP 连接终止序
列的前半部分。至此,照务器套接字处于 CLOSE_WAIT 状态,客户套接字则处于 FIN_WAIT_2
状态
-
当服务器 TCP 接收 FIN 时,服务器子进程阻塞于 readline 调用(图 5-3),于是 readline 返回 0。这导致 str_echo 函数返回服务器子进程的 main 函数。
-
服务器子进程通过调用 exit 来终止(服务器子进程3)。
-
服务器子进程中打开的所有描述符随之关闭。由子进程来关闭已连接套接字会引发 TCP 连接终止序列的最后两个分节:一个从服务器到客户的 FIN 和一个从客户到服务器的 ACK (图 2-5)。至此,连接完全终止,客户套接字进入 TIME_WAIT 状态。
注意
在服务器子进程终止时,给父进程发送一个 SIGCHLD 信号。这一点在本例中发生了,但是我们没有在代码中捕获该信号,而该信号的默认行为是被忽略。既然父进程未加处理,子进程于是进入僵死状态。我们可以使用 ps 命令验证这一点。
若客户端按 Ctrl + D 关闭,等于输入了 EOF 终止符,客户端会退出,服务端可以看到:
原来处于 wait_woken 的进程变成了僵尸进程,上图中有很多僵尸进程,因为我退出过很多客户端程序
3. 处理僵尸进程 - 通用服务器构建
主要内容
-
处理僵尸进程
- 处理被中断的系统调用(慢系统调用)
处理僵尸进程
变成僵尸进程的原因是因为子进程死了,但是父进程觉得将来可能用得到,所以先将其状态保存下来。我们显然不愿意留存僵死进程。它们占用内核中的空间,最终可能导致我们耗尽进程资源。
僵尸进程是一个已经完成执行但仍在进程表中的进程。虽然这个进程已经不再执行任何实际的任务,但它在进程表中占据一个位置,直到父进程调用 wait()
或 waitpid()
来获取其退出状态。
处理流程
- 子进程死后,会向父进程发送一个 SIGCHILD 信号
- 父进程要设置信号处理函数
- 父进程信号处理函数中调用
wait()
或waitpid()
来获取其退出状态。
这样就不会出现僵尸进程。
选用 waitpid
假设有五个客户连接到了服务器:
客户终止,关闭了 5 个连接,终止了五个子进程:
建立一个 wait 并不能防止出现僵尸进程。问题在于:5 个信号都是在信号处理函数之前产生,而信号处理函数只能处理 1 次,而信号又不会排队。所以只能处理一个信号,其他的都是僵尸进程。
正确的解决办法是调用 waitpid 而不是 wait,图 5-11 给出了正确处理 SIGCHLD 的 sig_chld 函数。这个版本管用的原因在于:我们在一个循环内调用 waitpid,以获取所有已终止子进程的状态。我们必须指定 WNOHANG 选项,它告知 waitpid 在有尚未终止的子进程在运行时不要阻塞。
通用服务器编程
- fork 子进程之前,必须编写捕获 SIGCHILD 信号的函数
- 捕获信号时,必须处理被中断的慢系统调用
- SIGCHILD 信号处理函数必须正确编写,防止僵尸进程
通用服务器函数(tcpserv04.c)
#include "unp.h"
int
main(int argc, char **argv)
{
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
void sig_chld(int);
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
//捕捉SIGCHILD信号
Signal(SIGCHLD, sig_chld); /* must call waitpid() */
for ( ; ; ) {
clilen = sizeof(cliaddr);
//捕获信号时,必须处理被中断的系统调用**
if ( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0) {
if (errno == EINTR)
continue; /* back to for() */
else
err_sys("accept error");
}
if ( (childpid = Fork()) == 0) { /* child process */
Close(listenfd); /* close listening socket */
str_echo(connfd); /* process the request */
exit(0);
}
Close(connfd); /* parent closes connected socket */
}
}
代码解释7
服务器套接字设置
- 3~21 行:设置套接字
捕捉信号,处理僵尸进程
- 24 行:signal 函数捕获子进程提交的 SIGCHILD,之后进入 sig_child 函数。
处理被中断的系统调用
- 30~35:处理被中断的系统调用8
处理被中断的系统调用
- 我们键入 EOF 字符来终止客户。客户 TCP 发送一个 FIN 给服务器,服务器响应以一个 ACK。
- 服务端子进程7阻塞中的 readline 读到 EOF,从而子进程终止。
- 当 SIGCHLD 信号递交时,父进程阻塞于 accept 调用。sig_chld 函数(信号处理函数)执行,其 wait 调用取到子进程的 PID 和终止状态,随后是 printf 调用,最后返回。
- 既然该信号是在父进程阻塞于慢系统调用(accept)时由父进程捕获的,内核就会使 accept 返回一个 EINTR 错误(被中断的系统调用) 。而父进程不处理该错误,于是中止。
因此我们需要处理被中断的系统调用 accept:
//捕获信号时,必须处理被中断的系统调用**
if ( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0) {
if (errno == EINTR)
continue; /* back to for() */
else
err_sys("accept error");
}
↩" />
1.2 服务端套接字设置 ↩
↩ ↩#include "unp.h" int main(int argc, char **argv) { // 第一部分:服务器套接字设置 int listenfd, connfd; pid_t childpid; socklen_t clilen; struct sockaddr_in cliaddr, servaddr; listenfd = Socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); Bind(listenfd, (SA *) &servaddr, sizeof(servaddr)); Listen(listenfd, LISTENQ); // 高并发编程 for ( ; ; ) { clilen = sizeof(cliaddr); connfd = Accept(listenfd, (SA *) &cliaddr, &clilen); // 第二部分:回射函数 if ( (childpid = Fork()) == 0) { /* child process */ Close(listenfd); /* close listening socket */ str_echo(connfd); /* process the request */ exit(0); } Close(connfd); /* parent closes connected socket */ } }
1.1 客户端套接字设置 ↩
↩ ↩#include "unp.h" int main(int argc, char **argv) { int sockfd; struct sockaddr_in servaddr; if (argc != 2) err_quit("usage: tcpcli <IPaddress>"); sockfd = Socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(SERV_PORT); Inet_pton(AF_INET, argv[1], &servaddr.sin_addr); Connect(sockfd, (SA *) &servaddr, sizeof(servaddr)); str_cli(stdin, sockfd); /* do it all */ exit(0); }
↩#include "unp.h" void str_cli(FILE *fp, int sockfd) { char sendline[MAXLINE], recvline[MAXLINE]; while (Fgets(sendline, MAXLINE, fp) != NULL) { Writen(sockfd, sendline, strlen(sendline)); if (Readline(sockfd, recvline, MAXLINE) == 0) err_quit("str_cli: server terminated prematurely"); Fputs(recvline, stdout); } }
↩ ↩#include "unp.h" int main(int argc, char **argv) { int listenfd, connfd; pid_t childpid; socklen_t clilen; struct sockaddr_in cliaddr, servaddr; void sig_chld(int); listenfd = Socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); Bind(listenfd, (SA *) &servaddr, sizeof(servaddr)); Listen(listenfd, LISTENQ); //捕捉SIGCHILD信号 Signal(SIGCHLD, sig_chld); /* must call waitpid() */ for ( ; ; ) { clilen = sizeof(cliaddr); //捕获信号时,必须处理被中断的系统调用** if ( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0) { if (errno == EINTR) continue; /* back to for() */ else err_sys("accept error"); } if ( (childpid = Fork()) == 0) { /* child process */ Close(listenfd); /* close listening socket */ str_echo(connfd); /* process the request */ exit(0); } Close(connfd); /* parent closes connected socket */ } }
处理被中断的系统调用 ↩
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于