1. 代码
服务端代码
#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); //((in_addr_t) 0x00000000) ==> uint32_t
servaddr.sin_port = htons(SERV_PORT); //9877
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ); //LISTENQ: 1024
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 */
}
}
void
str_echo(int sockfd)
{
ssize_t n;
char buf[MAXLINE];
again:
while ( (n = read(sockfd, buf, MAXLINE)) > 0) //从套接字读取数据,读的数据有效
//客户若关闭连接,收到客户发送的FIN,返回0
Writen(sockfd, buf, n); //回射给客户
if (n < 0 && errno == EINTR) //阻塞假错EINTR
goto again;
else if (n < 0) //真错
err_sys("str_echo: read error");
}
客户端代码
#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); //服务端服务端口9877
Inet_pton(AF_INET, argv[1], &servaddr.sin_addr); //命令行传入服务端地址
Connect(sockfd, (SA *) &servaddr, sizeof(servaddr));
str_cli(stdin, sockfd); /* do it all */
exit(0);
}
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); //打印到标准输出
}
}
服务端代码缺陷
- 子进程结束,父进程没有给子进程收尸,signal函数捕获SIGCHLD信号,sig_child函数是具体的实现,具体实现要用waitpid而不是wait(wait缺点:若同时多个子进程终止,会信号丢失,只处理一次。waitpid可以指定我们想等待的进程ID)
- accept属于慢系统调用,内核会使accept返回一个EINTR错误(被中段的系统调用)。要正确处理这种错误
针对缺陷改进
#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);
Signal(SIGCHLD, sig_chld); /* !!! 1. must call waitpid() !!!*/
for ( ; ; ) {
clilen = sizeof(cliaddr);
if ( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0) {
if (errno == EINTR)
continue; /* !!! 2. 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 */
}
}
2. accept返回前连接中止
如果我们在调用accept函数返回之前, 该客户端TCP发送了一个RST(复位)。在服务器中, 表现为该连接仍在TCP队列中, 等待服务器进程调用accept的时候RST到达。此时返回的套接字是一个已连接,但是却有接受了RST的套接字。POSIX规定返回的errno为ECONNABORTED,服务器可以忽略他,再次调用accept就行。
TCP异常处理(accept返回前连接中止)与SO_LINGER选项
3. 服务器进程终止
杀死服务端的对接客户端的子进程ID,SIGCHILD被父进程捕获,并正确处理。客户端阻塞在fgets准备读取标准输入的数据,所以服务端发送的FIN被接收却看不到/客户端继续向服务端发送数据,所以服务端会响应一个RST(已经终止),然而客户端在向服务端发送完数据后立即调用readline准备读取回射的字符串,但是readline会立即返回0(之前的FIN此时看到了),于是有了出错信息 "str_cli: server terminated prematurely"
4. SIGPIPE信号
向某个已经收到RST的套接字执行写操作时,内核向该进程发送一个SIGPIPE信号。
客户端测试代码
#include "unp.h"
void
str_cli(FILE *fp, int sockfd)
{
char sendline[MAXLINE], recvline[MAXLINE];
while (Fgets(sendline, MAXLINE, fp) != NULL) {//服务器进程终止发送FIN,客户端未被告知已收到FIN,仍继续发
//一句话分2次发,中间sleep 1s
Writen(sockfd, sendline, 1); //发完这句话会让服务端发送RST
sleep(1);
Writen(sockfd, sendline+1, strlen(sendline)-1);//引发SIGPIPE信号
if (Readline(sockfd, recvline, MAXLINE) == 0)
err_quit("str_cli: server terminated prematurely");
Fputs(recvline, stdout);
}
}