Socket-TCP编程
一子进程处理客户连接
程序:客户端代码
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/stat.h>
#include <sys/select.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <strings.h>
#include <string.h>
#include "err_common.h"
#define MAXLINE 4096
#define SA struct sockaddr
#define SERV_PORT 9877
#define max(a,b) (((a)>(b)) ? (a) :(b))
ssize_t writen(int fd, const void*vptr, size_t n);
ssize_t readline(int fd, void*vptr,size_t maxlen);
void str_cli(FILE *fp,int sockfd);
int main(int argc,char **argv)
{
int sockfd;
struct sockaddr_in servaddr;
if(argc != 2)
err_quit("usage:tcpcli<IPaddress>");
if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)
err_sys("Fail to create clisocket");
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(SERV_PORT);
inet_pton(AF_INET,argv[1],&servaddr.sin_addr);
if(connect(sockfd,(SA*)&servaddr,sizeof(servaddr))==-1)
err_sys("Fail to connectserver");
str_cli(stdin,sockfd);
exit(0);
}
void str_cli(FILE *fp,int sockfd)
{
int maxfdpl;
fd_set rset;
charsendline[MAXLINE],recvline[MAXLINE];
FD_ZERO(&rset);
for(;;){
FD_SET(fileno(fp),&rset);
FD_SET(sockfd,&rset);
maxfdpl=max(fileno(fp),sockfd)+1;
select(maxfdpl,&rset,NULL,NULL,NULL);
if(FD_ISSET(sockfd,&rset)){
if(readline(sockfd,recvline,MAXLINE)==0)
err_quit("str_cli:serverterminated prematurely");
fputs(recvline,stdout);
}
if(FD_ISSET(fileno(fp),&rset)){
if(fgets(sendline,MAXLINE,fp)==NULL)
return;
if(writen(sockfd,sendline,strlen(sendline))==-1)
err_sys("Fail to writen");
}
}
}
ssize_t readline(int fd, void*vptr,size_t maxlen)
{
ssize_t n, rc;
char c, *ptr;
ptr=vptr;
for(n=1;n<maxlen;n++){
if((rc=read(fd,&c,1))==1){
*ptr++=c;
if(c=='\n')
break;
}else if(rc==0){
if(n==1)
return(0);
else
break;
}else
return(-1);
}
*ptr=0;
return(n);
}
ssize_t writen(int fd, const void*vptr, size_t n)
{
size_t nleft;
ssize_t nwritten;
const char *ptr;
ptr=vptr;
nleft=n;
while(nleft>0){
if((nwritten=write(fd,ptr,nleft))<=0){
if(errno==EINTR)
nwritten=0;
else
return(-1);
}
nleft-=nwritten;
ptr+=nwritten;
}
return (n);
}
服务器端代码
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <strings.h>
#include <signal.h>
#include <unistd.h>
#include "err_common.h"
#define MAXLINE 4096
#define SERV_PORT 9877
#define LISTENQ 1024
#define SA struct sockaddr
void str_echo(int sockfd);
ssize_t readline(int fd, void*vptr,size_t maxlen);
ssize_t writen(int fd, const void*vptr, size_t n);
void sig_child(int signo);
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);
if(listenfd==-1)
err_sys("Fail to create serversocket.");
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
servaddr.sin_port=htons(SERV_PORT);
if((bind(listenfd,(SA*)&servaddr,sizeof(servaddr)))==-1)
err_sys("Fail to bindservaddr.");
if(listen(listenfd,LISTENQ)==-1)
err_sys("Fail to listen");
if(signal(SIGCHLD,sig_child)==SIG_ERR)
err_sys("Fail to signalSIGCHLD");
for(;;){
clilen=sizeof(cliaddr);
if((connfd=accept(listenfd,(SA*)&cliaddr,&clilen))==-1)
err_sys("Fail to accept");
childpid=fork();
if(childpid==0){
close(listenfd);
str_echo(connfd);
exit(0);
}
else if(childpid==-1)
err_sys("Fail to fork");
if(close(connfd)==-1)
err_sys("Fail to closesocket");
}
return 0;
}
void str_echo(int sockfd)
{
ssize_t n;
char line[MAXLINE];
for(;;){
if((n=readline(sockfd,line,MAXLINE))==0)
return;
else if(n==-1)
err_sys("Fail to readline");
if(writen(sockfd,line,n)==-1)
err_sys("Fail to writen");
}
}
ssize_t readline(int fd, void*vptr,size_t maxlen)
{
ssize_t n, rc;
char c, *ptr;
ptr=vptr;
for(n=1;n<maxlen;n++){
if((rc=read(fd,&c,1))==1){
*ptr++=c;
if(c=='\n')
break;
}else if(rc==0){
if(n==1)
return(0);
else
break;
}else
return(-1);
}
*ptr=0;
return(n);
}
ssize_t writen(int fd, const void*vptr, size_t n)
{
size_t nleft;
ssize_t nwritten;
const char *ptr;
ptr=vptr;
while(nleft>0){
if((nwritten=write(fd,ptr,nleft))<=0){
if(errno==EINTR)
nwritten=0;
else
return(-1);
}
nleft-=nwritten;
ptr+=nwritten;
}
return (n);
}
void sig_child(int signo)
{
pid_t pid;
int stat;
pid=wait(&stat);
printf("child %dterminated\n",pid);
return;
}
1中断的系统调用。当一个进程阻塞于慢系统调用时捕获到一个信号,等到信号返回时,系统调用可能返回一个EINTR错误。有些内核自动重启某些被中断的系统调用。
SA_RESTART标志
2处理SIGCHLD信号,避免僵死进程。因为僵死进程会占用内核空间。
正确处理SIGCHLD信号的方法,用wait不足以防止出现僵死进程,问题时Unix信号一般是不排队的,当多个客户几乎同时终止时,就会有僵死进程留下,而且留下的个数还不定。
正确方法应该是用waitpid,指定选项WNOHANG,它告诉waitpid在有未终止的子进程运行时不要阻塞。不能在循环中调用wait,因为没有办法防止wait在有未终止的子进程运行时阻塞。
Void sig_chld(int signo){
pid_t pid;
int stat;
while((pid=waitpid(-1, &stat,WNOHANG))>0)
printf(“child %d terminated\n”,pid);
return;}
3 accept返回前连接夭折
三路握手完成后,连接建立,然后客户发一个RST(复位),若此时服务器端accept还未返回,则accept返回一个非致命错误,此时需要再调用一次accept。
这种情况下,accept可能返回EPROTO(但是子系统中出现某些致命的协议相关事件时也返回EPROTO,因此不可能知道是否再次调用accept),Posix.1g规定返回是ECONNABORTED,在这种情况下,服务器可以忽略错误而简单地再调用accept一次。
如何设置套接口选项SO_LINGER以生成RST.
4 服务器子进程终止
启动客户-服务器,然后杀死服务器子进程,模拟服务器进程的崩溃。此时客户TCP从服务器接收FIN并以ACK响应,但问题是客户进程正阻塞于fgets调用。netstat观察进程状态,可以发现客户进程TCP连接处于CLOSE_WAIT,服务器TCP连接处于FIN_WIAT_2状态,至此已完成TCP连接终止序列的一半。
在客户端再输入一行,客户TCP给服务器发送数据(半关闭),这是允许的。服务器接收到来自客户的数据时,由于先前打开那个套接口的进程已终止,所以以RST响应。由于客户进程在调用writen后立即调用readline,由于先前接收的服务器终止时发送的FIN,readline立即返回0(文件结束符),所以客户进程看不到RST。由于客户不希望在此时接收文件结束符,所以出现错误信息”serverterminated prematurely”(服务器过早终止)。
出现这种现象的原因是,当FIN到达客户套接口时,客户阻塞于fgets调用。客户此时有两个描述符--套接口和用户输入,它不能只阻塞于两个源中某个特定源输入,而应是阻塞于任一源的输入。(select和poll)。
Tcpdump必须是root权限。
4 SIGPIPE信号
当一个进程向接收了RST的套接口进行写操作时,内核给该进程发一个SIGPIPE信号。该信号的缺省行为就是终止进程,所以进程必须捕获它以免不情愿地别被终止。
必须意识到,如果使用了多个套接口,改信号的递交无法告诉我们是那个套接口的错,如果我们确实需要知道是那个write出了错,那么必须要么不理会该信号,要么从信号处理程序返回后再处理来自write的EPIPE。
5启动服务-客户,客户发送数据回显,然后服务器主机崩溃,此时客户再发数据,客户TCP不断重传数据,客户阻塞于readline的调用,直到超时,此时返回一个错误ETIMEDOUT。同样的情况下若中间路由器判定服务器主机不可达,会以一个目的不可达的ICMP消息响应,错误是EHOSTUNREACH或ENETUNREACH。
如果要实现不主动发送数据也想检测出服务器主机的崩溃,那就需要另一技术--套接口选项SO_KEEPALIVE。
6 服务器主机崩溃后重启
我们启动服务器和客户,并键入一行以确认连接已建立。服务器主机崩溃并重启。在客户输入一行,他将作为TCP数据分节发往服务器主机,由于服务器主机崩溃重启时,它的TCP丢失崩溃前的所有连接信息,所以服务器TCP对接收的客户分节以RST响应。当RST到达时,客户阻塞于readline调用,导致它返回ECONNRESET错误。
7 服务器主机关机
当服务器进程正在运行时,操作员关闭该服务器将会发生什么。
当unix系统关系时,一般是有init进程给所有进程发信号SIGTERM(此信号可捕获),等待一段固定时间(5-20秒),然后给还在运行的所有进程发信号SIGKILL(此信号不可捕获)。这就给所有运行进程一小段时间来清除和终止。若我们不捕获SIGTERM终止,服务器将由信号SIGKILL终止。当进程终止时,所有打开的描述字都关闭,将同样引起半关闭的情况,此时我们同样需要在客户上使用函数select和poll,使得客户在服务器进程开始终止时就检测到。
8 服务器和客户机间传递二进制格式数据要小心,一般把树值数据作为文本串来传递。
10 str_cli此段代码在批量方式下,输入文件的结束符的处理并不意味着我们已经完成了从套接口的读入,可能仍有请求在去服务器的路上,或者在我往客户的路上仍有应答。
11 shutdown
终止网络连接的正常方法是调用close,但close有两个限制。一是close将描述字的访问计数减1,仅在此计数为0时才关闭接口;二是close终止了数据传送的两个方向,读和写。TCP连接是全双工的,有很多时候我们要通知另一端我们已完成了数据发送,即使那一端仍有许多数据要发送也是如此,因此原来的str_cli在批量输入时会有问题。
shutdown---SHUT_RD, SHUT_WR, SHUT_RDWR.
12下面是str_cli的改进版本,它使用了select和shutdown,前者在服务器关闭它这一端的连接时通知我们,后者使我们正确处理批量输入。
void str_cli_r(FILE *fp,int sockfd)
{
int maxfdpl, stdlineof;
fd_set rset;
char sendline[MAXLINE],recvline[MAXLINE];
stdlineof=0;
FD_ZERO(&rset);
for(;;){
if(stdlineof==0)
FD_SET(fileno(fp),&rset);
FD_SET(sockfd,&rset);
maxfdpl=max(fileno(fp),sockfd)+1;
select(maxfdpl,&rset,NULL,NULL,NULL);
if(FD_ISSET(sockfd,&rset)){
if(readline(sockfd,recvline,MAXLINE)==0){
if(stdlineof==1)
return;
else
err_quit("str_cli:serverterminated prematurely");
}
fputs(recvline,stdout);
}
if(FD_ISSET(fileno(fp),&rset)){
if(fgets(sendline,MAXLINE,fp)==NULL){
stdlineof=1;
shutdown(sockfd,SHUT_WR);
FD_CLR(fileno(fp),&rset);
continue;
}
if(writen(sockfd,sendline,strlen(sendline))==-1)
err_sys("Fail to writen");
}
}
}
二用select实现服务器
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <strings.h>
#include <signal.h>
#include <unistd.h>
#include "err_common.h"
#define MAXLINE 4096
#define SERV_PORT 9877
#define LISTENQ 1024
#define SA struct sockaddr
ssize_t readline(int fd, void*vptr,size_t maxlen);
ssize_t writen(int fd, const void*vptr, size_t n);
int main(int argc, char **argv)
{
int i, maxi, maxfd, listenfd,connfd, sockfd;
int nready, client[FD_SETSIZE];
ssize_t n;
fd_set rset, allset;
char line[MAXLINE];
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
if((listenfd = socket(AF_INET,SOCK_STREAM, 0))==-1)
err_sys("Fail to create serversocket.");
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr =htonl(INADDR_ANY);
servaddr.sin_port =htons(SERV_PORT);
if(bind(listenfd, (SA *) &servaddr,sizeof(servaddr))==-1)
err_sys("Fail to bindservaddr.");
if(listen(listenfd, LISTENQ)==-1)
err_sys("Fail to listen");
maxfd = listenfd; /* initialize */
maxi = -1; /* index into client[]array */
for (i = 0; i < FD_SETSIZE; i++)
client[i] = -1; /* -1 indicatesavailable entry */
FD_ZERO(&allset);
FD_SET(listenfd, &allset);
/* end fig01 */
/* include fig02 */
for ( ; ; ) {
rset = allset; /* structureassignment */
nready = select(maxfd+1, &rset,NULL, NULL, NULL);
if (FD_ISSET(listenfd, &rset)){ /* new client connection */
clilen = sizeof(cliaddr);
connfd = accept(listenfd, (SA *)&cliaddr, &clilen);
#ifdef NOTDEF
printf("new client: %s, port%d\n",
inet_ntop(AF_INET,&cliaddr.sin_addr, 4, NULL),
ntohs(cliaddr.sin_port));
#endif
for (i = 0; i < FD_SETSIZE; i++)
if (client[i] < 0) {
client[i] = connfd; /* savedescriptor */
break;
}
if (i == FD_SETSIZE)
err_quit("too many clients");
FD_SET(connfd, &allset); /* addnew descriptor to set */
if (connfd > maxfd)
maxfd = connfd; /* for select */
if (i > maxi)
maxi = i; /* max index inclient[] array */
if (--nready <= 0)
continue; /* no more readabledescriptors */
}
for (i = 0; i <= maxi; i++) { /*check all clients for data */
if ( (sockfd = client[i]) < 0)
continue;
if (FD_ISSET(sockfd, &rset)) {
if ( (n = readline(sockfd, line,MAXLINE)) == 0) {
/*4connection closed by client */
close(sockfd);
FD_CLR(sockfd, &allset);
client[i] = -1;
} else
writen(sockfd, line, n);
if (--nready <= 0)
break; /* no more readabledescriptors */
}
}
}
}
/* end fig02 */
ssize_t readline(int fd, void*vptr,size_t maxlen)
{
ssize_t n, rc;
char c, *ptr;
ptr=vptr;
for(n=1;n<maxlen;n++){
if((rc=read(fd,&c,1))==1){
*ptr++=c;
if(c=='\n')
break;
}else if(rc==0){
if(n==1)
return(0);
else
break;
}else
return(-1);
}
*ptr=0;
return(n);
}
ssize_t writen(int fd, const void*vptr, size_t n)
{
size_t nleft;
ssize_t nwritten;
const char *ptr;
ptr=vptr;
while(nleft>0){
if((nwritten=write(fd,ptr,nleft))<=0){
if(errno==EINTR)
nwritten=0;
else
return(-1);
}
nleft-=nwritten;
ptr+=nwritten;
}
return (n);
}
拒绝服务型攻击,若一个恶意客户连接到服务器上,发送一个字节的数据(而不是一行数据)后睡眠,服务器调用readline,它从客户读到单个字节的数据,然后阻塞于下一个read调用,再也不能为其他客户提供服务。解决办法,a)使用非阻塞I/O模型;b)让每个客户由单独的控制线程提供服务;c)对I/O操作设置超时。
1 pselect理解信号丢失?信号阻塞?poll函数?