Linux下的socket编程实践(四)TCP服务端优化和常见函数


并发下的僵尸进程处理

只有一个进程连接的时候,我们可以使用以下两种方法处理僵尸进程:

 

1)通过忽略SIGCHLD信号,避免僵尸进程

在server端代码中添加

signal(SIGCHLD, SIG_IGN);

2)通过wait/waitpid方法,解决僵尸进程

 

signal(SIGCHLD,onSignalCatch);  
  
void onSignalCatch(int signalNumber)  
{  
    wait(NULL);  
}  
那么如果是多进程状态下多个客户端同时关闭呢?

 

\\

我们可以用下面的客户端代码测试:

 

/** client端实现的测试代码**/  
int main()  
{  
    int sockfd[50];  
    for (int i = 0; i < 50; ++i)  
    {  
        if ((sockfd[i] = socket(AF_INET, SOCK_STREAM, 0)) == -1)  
            err_exit("socket error");  
  
        struct sockaddr_in serverAddr;  
        serverAddr.sin_family = AF_INET;  
        serverAddr.sin_port = htons(8001);  
        serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");  
        if (connect(sockfd[i], (const struct sockaddr *)&serverAddr, sizeof(serverAddr)) == -1)  
            err_exit("connect error");  
    }  
    sleep(20);  
}  
此时由于信号的同时到达,并且SIGCHLD又是不可靠信号,不支持排队,会留下相当部分的僵尸进程

 

\
解决方法:

使用循环的 waitpid函数就可以将所有的子进程留下的僵尸进程处理掉

 

void sigHandler(int signo)  
{  
    while (waitpid(-1, NULL, WNOHANG) > 0)  
        ;  
} 
//pid=-1 等待任何子进程,相当于 wait()。
//WNOHANG 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若结束,则返回该子进程的ID。
地址查询的API

 

 

#include   
int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //获取本地addr结构  
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //获取对方addr结构  
  
int gethostname(char *name, size_t len);  
int sethostname(const char *name, size_t len);  
  
#include   
extern int h_errno;  
struct hostent *gethostbyname(const char *name);  
  
#include        /* for AF_INET */  
struct hostent *gethostbyaddr(const void *addr, socklen_t len, int type);  
struct hostent *gethostent(void);  
//hostent结构体  
struct hostent  
{  
    char  *h_name;            /* official name of host */  
    char **h_aliases;         /* alias list */  
    int    h_addrtype;        /* host address type */  
    int    h_length;          /* length of address */  
    char **h_addr_list;       /* list of addresses */  
}  
#define h_addr h_addr_list[0] /* for backward compatibility */ 

 

这两个函数调用的时机很重要,否则不能得到正确的地址和端口:

TCP

对于服务器来说,在bind以后就可以调用getsockname来获取本地地址和端口,虽然这没有什么太多的意义。getpeername只有在链接建立以后才调用,否则不能正确获得对方地址和端口,所以他的参数描述字一般是链接描述字而非监听套接口描述字。

 

对于客户端来说,在调用socket时候内核还不会分配IP和端口,此时调用getsockname不会获得正确的端口和地址(当然链接没建立更不可能调用getpeername),当然如果调用了bind 以后可以使用getsockname。想要正确的到对方地址(一般客户端不需要这个功能),则必须在链接建立以后,同样链接建立以后,此时客户端地址和端口就已经被指定,此时是调用getsockname的时机。

 

UDP

UDP分为链接和没有链接2种(这个到UDP与connect可以找到相关内容)

 

没有链接的UDP不能调用getpeername,但是可以调用getsockname,和TCP一样,他的地址和端口不是在调用socket就指定了,而是在第一次调用sendto函数以后

 

已经链接的UDP,在调用connect以后,这2个函数都是可以用的(同样,getpeername也没太大意义。如果你不知道对方的地址和端口,不可能会调用connect)。

获取本机所有IP:

 

/**获取本机IP列表**/  
int gethostip(char *ip)  
{  
    struct hostent *hp = gethostent();  
    if (hp == NULL)  
        return -1;  
  
    strcpy(ip, inet_ntoa(*(struct in_addr*)hp->h_addr));  
    return 0;  
}  
  
int main()  
{  
    char host[128] = {0};  
    if (gethostname(host, sizeof(host)) == -1)  
        err_exit("gethostname error");  
  
    cout << "host-name: " << host << endl;  
    struct hostent *hp = gethostbyname(host);  
    if (hp == NULL)  
        err_exit("gethostbyname error");  
  
    cout << "ip list: " << endl;  
    for (int i = 0; hp->h_addr_list[i] != NULL; ++i)  
    {  
        cout << '\t'  
             << inet_ntoa(*(struct in_addr*)hp->h_addr_list[i]) << endl;  
    }  
  
    char ip[33] = {0};  
    gethostip(ip);  
    cout << "local-ip: " << ip << endl;  
}  
简述TCP 11种状态

 

\

1.客户端和服务器连接建立的时候,双方处于ESTABLISHED(建立)状态

\
2.关于TIME_WAIT状态 详见 http://www.mamicode.com/info-detail-190400.html

3.TCP/IP协议的第11种状态:图上只包含10种状态,还有一种CLOSING状态

产生CLOSING状态的原因:

Server端与Client端同时关闭(同时调用close,此时两端同时给对端发送FIN包),将产生closing状态,最后双方都进入TIME_WAIT状态。(因为主动关闭的一方会进入TIME_WAIT状态,双方同时主动关闭,则都进入)

 

SIGPIPE信号

 

往一个已经接收FIN的套接中写是允许的,接收到FIN仅仅代表对方不再发送数据;但是在收到RST段之后,如果还继续写,调用write就会产生SIGPIPE信号,对于这个信号的处理我们通常忽略即可。

signal(SIGPIPE, SIG_IGN);

 

SIGPIPE,虽然已经接受到FIN,但是我还可以发送数据给对方;如果对方已经不存在了,那么TCP会进行重置,TCP协议栈发送RST段,收到RST后,再进行write会产生SIGPIPE信号。

其实很好理解:TCP可以看作是一个全双工的管道,读端信号不存在了,如果再对管道进行写的话会导致SIGIPPE信号的产生,处理的时候是忽略这个信号就可以了,其实就是按照管道的规则。

 

我们测试的时候在Client发送每条信息都发送两次,当Server端关闭之后Server端会发送一个FIN分节给Client端, 第一次消息发送之后, Server端会发送一个RST分节给Client端;第二次消息发送(调用write)时, 会产生SIGPIPE信号。

close和shutdown函数的区别

 

#include   
int close(int fd);  
  
#include   
int shutdown(int sockfd, int how);  

shutdown的how参数

SHUT_RD

关闭读端

SHUT_WR

关闭写端

SHUT_RDWR

读写均关闭

close终止了数据传送的两个方向,shutdown可以有选择的终止某个方向的数据传送或者终止数据传送的两个方向

 

shutdowm how=1就可以保证对等方接收到一个EOF字符,而不管其他进程是否已经打开可套接字。

而close不能保证,直到套接字引用计数减为0的时候才发送。也就是说知道所有进程都关闭了套接字。

调用close函数,可能导致全双工的管道还没有回射给客户端时,产生丢失数据现象;例如

FIN D C B A

A B C D ----》丢失,已经关闭close

Close准确的含义是 套接字引用计数减为0 的时候,才发送FIN

int conn;
conn=accept(sock,NULL,NULL);
pid_t pid=fork();
if(pid==-1)
   ERR_EXIT("fork"); 
if(pid==0)
{
	close(sock);
	//通信
	close(conn); //这时才会向对方发送FIN段(因为这个时候conn引用计数减为0) 
}
else if(pid>0)
   close (conn); //不会向客户端发送FIN段,仅仅只是将套接字的引用计数减1 

相关内容