TCP服务器端/客户端流程
- 监听套接字(listening socket)描述符
- 已连接套接字(connected socket)描述符
两个客户的TCP客户/服务器:
服务器主机上有两个已连接套接字,其中每一个都有各自的套接字接收缓冲区。
一个回射并发服务器执行如下步骤:
(1) 客户从标准输入读入一行文本,并写给服务器;
(2) 服务器从网络输入读入这行文本,并回射给客户;
(3) 客户从网络输入读入这行回射文本,并显示在标准输出上。
TCP回射并发服务器端流程
- 外地IP地址和外地端口必须在客户调用connect时指定
- 本地IP地址和本地端口通常由内核作为connect的一部分来选定,客户可以在连接建立后通过调用getsockname获取由内核指定的两个本地值
int main()
{
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
void sig_chld(int);
if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("Creating socket failed.");
exit(1);
}
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family=AF_INET;
//捆绑通配地址是在告知系统:要是系统是多宿主机,将接受目的地址为任何本地接口的连接
servaddr.sin_addr.s_addr= htonl (INADDR_ANY);
servaddr.sin_port=htons(9877);
if(bind(listenfd, (struct sockaddr *)& servaddr, sizeof(servaddr)) == -1)
{
perror("Bind error.");
exit(1);
}
if(listen(listenfd,BACKLOG)== -1)
{
perror("listen()error\n");
exit(1);
}
signal(SIGCHLD, sig_chld); //必须调用waitpid()
for(;;)
{
clilen =sizeof(cliaddr);
if((connfd = accept(listenfd,(struct sockaddr*)& cliaddr,& clilen)) == -1)
{
if(errno == EINTR)
continue;
else
perror("accept()error\n");
exit(1);
}
printf("connection from client ip: %s, prot: %d \n", inet_ntoa(cliaddr.sin_addr), htons(cliaddr.sin_port));
//fork为每个客户派生一个处理它们的子进程
if((childpid = fork()) == 0) {
//子进程关闭监听套接字
close(listenfd);
//子进程调用str_echo处理客户
str_echo(connfd);
exit(0);
}
//父进程关闭已连接套接字
close(connfd);
}
}
void sig_chld(int signo)
{
pid_t pid;
int stat;
//不能在循环内调用wait,wait会在正运行的子进程尚有未终止时阻塞,不足以防止出现僵尸进程
while((pid = waitpid(-1, &stat, WNOHANG)) > 0)
printf(“child %d terminated\n”, pid);
return;
}
// str_echo函数执行处理每个客户的服务:从客户读入数据,并回射给客户
void str_echo(int sockfd)
{
int n;
char buf[MAXLINE];
again:
//read函数从套接字读取数据
while((n = read(sockfd, buf, MAXLINE)) > 0)
//write函数将内容回射给客户
write(sockfd,buf,n);
if(n<0 && errno == EINTR)
goto again;
else if(n < 0) {
perror("str_echo read error\n");
exit(1);
}
//客户关闭连接,接收到客户的FIN将导致服务器子进程的read函数返回0,str_echo函数返回,子进程终止
}
TCP客户端流程
客户建立5个与服务器的连接
- 本地端口(服务器的众所周知端口)由bind指定。bind调用中服务器指定的本地IP地址通常是通配IP地址。如果服务器在一个多宿主机上绑定通配IP地址,那么它可以在连接建立后通过调用getsockname来确定本地IP地址。
- 如果另外一个程序由调用accept的服务器通过调用exec来执行,那么这个新程序可以在必要时调用getpeername来确定客户的IP地址和端口号
- 外地IP地址和外地端口由accept调用返回给服务器。
int main(int argc, char *argv[])
{
int i, sockfd[5];
struct sockaddr_in servaddr;
if (argc!=2)
{
perror ("Usage:%s <IP Address>\n",argv[0]);
exit(1);
}
//建立多个连接的目的是从并发服务器上派生多个子进程
for(i = 0; i<5;i++)
{
if((sockfd=socket(AF_INET, SOCK_STREAM, 0))==-1)
{
perror ("socket()error\n");
exit(1);
}
bzero(&servaddr,sizeof(servaddr));
//用服务器的IP地址和端口号装填一个网际网套接字地址结构
servaddr.sin_family= AF_INET;
servaddr.sin_port = htons(9877);
inet_pton(AF_INET, argv[1], &servaddr.sin_addr) ;
if(connect(sockfd,(struct sockaddr *)& servaddr,sizeof(servaddr))==-1)
{
perror ("connect()error\n");
exit(1);
}
}
//调用str_cli函数时仅用第一个连接
str_cli(stdin, sockfd[0]);
exit(0);
}
//str_cli函数完成客户处理循环:从标准输入读入一行文本,写到服务器,读回服务器对该行的回射,并把回射行写到标准输出上。
void str_cli(FILE *fp, int sockfd)
{
int maxfdp1, stdineof;
fd_set rset;
char buf[MAXLINE];
int n;
stdineof = 0;
FD_ZERO(&rset);
for ( ; ; )
{
if (stdineof == 0)
FD_SET(fileno(fp), &rset);
FD_SET(sockfd, &rset);
maxfdp1 = max(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
err_quit("str_cli: server terminated prematurely");
}
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;
}
writen(sockfd, buf, n);
}
}
}
当客户终止时,所有打开的描述符由内核自动关闭(仅需调用exit),且所有5个连接基本在同一时刻终止,这就引发5个FIN,反过来使服务器的5个子进程基本在同一时刻终止,导致差不多在同一时刻有5个SIGCHLD信号传递给父进程。
TCP服务器/客户端边界条件
1) accept返回前连接终止
三路握手完成,连接建立。
客户端:TCP发送一个RST(复位)
服务器:连接已由TCP排队,服务器进程调用accept之前RST到达
1) accept返回前连接终止
三路握手完成,连接建立。
客户端:TCP发送一个RST(复位)
服务器:连接已由TCP排队,服务器进程调用accept之前RST到达
SVR4实现返回一个EPROTO(Protocol error,协议错误)errno值
POSIX实现返回一个ECONNABORTED (Software caused connection abort )errno值
服务器忽略错误,并再次调用accept
2) 拒绝服务型攻击
当一个服务器在处理多个客户时,它绝对不能阻塞于只与单个客户相关的某个函数调用。否则可能导致服务器被挂起,拒绝为所有其他客户提供服务。即所谓拒绝服务(denial of service)型攻击。
可解决办法包括:
POSIX实现返回一个ECONNABORTED (Software caused connection abort )errno值
服务器忽略错误,并再次调用accept
2) 拒绝服务型攻击
当一个服务器在处理多个客户时,它绝对不能阻塞于只与单个客户相关的某个函数调用。否则可能导致服务器被挂起,拒绝为所有其他客户提供服务。即所谓拒绝服务(denial of service)型攻击。
可解决办法包括:
- 使用非阻塞式I/O
- 让每个客户有单独的控制线程提供服务(为每个客户创建一个子进程或一个线程)
- 对I/O操作设置一个超时