版本一
- 客户
include "unp.h"
void str_cli(FILE* fp, int sockfd);
int main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in server_name;
socklen_t server_name_len = sizeof(server_name);
if (argc != 2)
err_quit("usage: IP address");
if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
err_quit("socket error");
bzero(&server_name, sizeof(server_name));
server_name.sin_family = AF_INET;
server_name.sin_port = htons(8080);
inet_pton(AF_INET, argv[1], &server_name.sin_addr);
connect(sockfd, (SA*)&server_name, server_name_len);
str_cli(stdin, sockfd);
}
void str_cli(FILE* fp, int sockfd)
{
char sendline[MAXLINE], recvline[MAXLINE];
while( fgets(sendline, MAXLINE, fp) != NULL)
{
write(sockfd, sendline, strlen(sendline));
if ( readline(sockfd, recvline, MAXLINE) == 0)
err_quit("str_cli: server terminated prematurely");
fputs(recvline, stdout);
}
}
- 服务器
#include "unp.h"
void str_echo(int sockfd);
int main(int argc, char** argv)
{
int listenfd, connfd;
struct sockaddr_in name, client_name;
socklen_t client_name_len;
pid_t child;
if ( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
err_quit("socket error");
bzero(&name, sizeof(name));
name.sin_family = AF_INET;
name.sin_port = htons(8080);
name.sin_addr.s_addr = htonl(INADDR_ANY);
if ( bind(listenfd, (SA*)&name, sizeof(name)) < 0)
err_quit("bind");
if ( listen(listenfd, LISTENQ) < 0)
err_quit("listen");
for (; ;) {
connfd = accept(listenfd, (SA*)&client_name, &client_name_len);
if ( (child = fork()) == 0) {
close(listenfd);
str_echo(connfd);
exit(0);
}
close(connfd);
}
}
void str_echo(int sockfd)
{
ssize_t n;
char buf[MAXLINE];
while( (n = read(sockfd, buf, MAXLINE)) > 0)
write(sockfd, buf, n);
if (n < 0)
err_sys("str_echo: read error");
}
- 启动服务器:
./s.out
netstat -nlp | grep 8080 // 查询监听端口为8080的网络进程
tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN 11459/s.out
- 启动客户
./c.out 127.0.0.1
此时客户调用 socket 和 connect,后者引起TCP三路握手,当三路握手完成后,客户阻塞于 str_cli 中的 fgets 中,因为我们还没有输入什么。
而服务器在accept, fork后,子进程调用str_echo,然后阻塞于read之中。另一方面,父进程再次调用accept阻塞,等待客户连接。
至此有3个进程都在睡眠,客户进程,服务器父进程,服务器子进程。
- 正常终止
- 当我们在客户上键入EOF,fgets返回一个空指针,于是str_cli函数返回main,客户进程终止。
- 进程终止时会关闭所有打开描述符,客户TCP将会发送一个FIN给服务器。服务器发送ACK响应,此时服务器套接字处于CLOSE_WAIT,客户套接字处于FIN_WAIT_2状态。
- 当服务器接收到FIN时,服务器子进程阻塞于readline调用,于是readline返回0。str_echo函数返回,服务器子进程调用exit终止。
- 服务器子进程打开的描述符随之关闭,这回导致发送最后2个分节,一个从服务器到客户的FIN和一个从客户到服务器的ACK。
- 服务器子进程终止时,会给父进程发送一个SIGCHLD信号。该信号的默认行为是忽略,因为我们没有回收子进程,于是子进程变成僵尸进程。
amdin 11496 0.0 0.0 0 0 pts/14 Z+ 20:50 0:00 [s.out]
版本二
增加服务器回收子进程的处理,防止僵尸进程,仍然有缺陷。
#include "unp.h"
void etr_echo(int sockfd);
void sig_chld(int signo);
int main(int argc, char **argv)
{
......
signal(SIGCHLD, sig_chld);
for ( ; ;)
{
if ( (connfd = accept(listenfd, (SA*)&cliaddr, &clilen)) < 0)
if (errno == EINTR)
continue;
else
err_sys("acccept error");
if ( (child = fork()) == 0)
{
close(listenfd);
str_echo(connfd);
exit(0);
}
close(connfd);
}
}
void sig_chld(int signo)
{
pid_t pid;
int stat;
pid = wait(&stat); // 回收子进程的资源
printf("child %d terminated\n", pid);
return;
}
但是这个版本仍然有问题,因为POSIX的系统上信号处理有以下几点:
1、一旦安装了信号处理函数们就会一直安装上。(早期的系统是执行一次 就拆除一次)
2、在一个信号处理函数运行期间,正在被递交的信号是阻塞的。
3、如果一个信号在被阻塞期间产生了一次或多次,那么该信号被接触阻塞之后通常只递交一次,亦即是说,Unix的信号默认是不排队的。
所以假设我们模拟5个客户连接,然后再让这5个客户同一时间终止,就会使服务器的5个进程在同一时刻终止,也就意味着同时有5个SIGCHLD信号递交给父进程,而根据上面信号特点。这意味着并不能将子进程全部回收掉。
// 模拟5个客户连接
for (int i= 0; i < 5; i++) {
sockfd[i] = socket(AF_INET, SOCK_STREAM, 0);
bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(8080);
inet_pton(sockfd[i], argv[1], &server.sin_addr);
connect(sockfd[i], (SA*)&server, sizeof(server));
}
str_cli(stdin, sockfd[0]);
exit(0);
// 测试果然有僵尸进程
amdin 12344 0.0 0.0 0 0 pts/2 Z+ 00:08 0:00 [s1.out]
amdin 12345 0.0 0.0 0 0 pts/2 Z+ 00:08 0:00 [s1.out]
amdin 12346 0.0 0.0 0 0 pts/2 Z+ 00:08 0:00 [s1.out]
版本三
// 仅仅修改信号处理函数
void sig_chld(int signo)
{
pid_t pid;
int stat;
while( (pid = waitpid(-1, &stat, WNOHANG)) > 0)
printf("child %d terminated\n", pid);
return;
}
- wait和waitpid的区别
#include
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
均返回:若成功则返回进程ID,若出错则为0或者-1
如果调用wait的进程没有已终止的子进程,不过有一个或多个子进程人仍在执行,那么wait将一直阻塞到现有子进程第一个终止为止。
而waitpid给了我们更多的控制:
pid参数表示我们想等待的进程ID参数,-1表示等待第一个终止的子进程。
options参数允许我们指定附加选项。最常用的的选项是WNOHANG,它告知内核在没有已终止子进程时不要阻塞。
特殊情况处理
- 服务器主机崩溃
TCP客户会发送数据到服务器,然后等待服务器发回的ACK。此时由于没有收到ACK,TCP客户会持续重传数据分节,超过一定时间后会发送一个RST(从首次重传数据分节到最后发送RST,时间大概为9分钟左右)。 - 服务器崩溃后重启
如果服务器崩溃,客户不主动给服务器发送数据,那么它将不会知道服务器主机已经崩溃。当服务器主机崩溃后重启时,它的TCP丢失了崩溃前的所有连接信息,因此服务器TCP对于所收到的来自客户的数据分节响应一个RST。
参考资料
《UNIX 网络编程》3th [美] W.Richard Stevens,Bill Fenner,Andrew M. Rudoff