如何写一个完美的socket多线程通信程序 ?(持续更新中...)

/*
* 由socket多线程通信中一些常见问题与技巧引发的一系列程序开发秘籍,(个人经验,仅供参考)

* 请结合http://blog.csdn.net/ipromiseu/archive/2009/05/12/4169589.aspx 《Linux系统下的多线程编程全面入门 》阅读本文
*          by . Gray Luo([email protected])
*/



1.socket多线程通信中一些常见问题与技巧:

1>socket考虑到效率,一般采用nonblock方式。

2>socket通信或者其它读写操作,使用select进行可读写的检测,一方面可以防止阻塞占用CPU,另一方面可以安全地进行数据读写。

3>read/write 或 recv/send 操作之前都需要使用FD_ISSET()检测句柄是否可读写。

4>read/write 或 recv/send 都需要重新封装以达到对数据的完整收发与出错处理。 请参考我的另一篇文章《
通信中如何一次性完整地接收数据》 http://blog.csdn.net/ipromiseu/archive/2010/01/05/5138760.aspx

5>对出错的socket,如果无法恢复,则需要销毁,而不能继续进行读写。

6>一个多次循环的操作内,如while(),for()之中的表达式,一定要在每次循环后加上usleep()让出CPU,usleep的长短需要调节以适应程序的各个模块的效率。在这种无限循环中,循环条件需要是一个变量,可不能写成while(1),for(;;)之类的,因为如果写成这样,那当程序出现异常时,可能程序就无法正常退出,对程序资源造成混乱。

7>socket的listen位置,来一个新的连接,就开一个新的线程去处理,这里的线程ID也需要新的ID。这里容易出现bug.

8>socket通信中不能严格按照收发顺序来进行收发数据,即一个socket的通信流程要简洁,彼此之间不能有太多的依赖性。

9>收发数据需要有magic_num/checksum,对于视频音频流可能还需要sequence num,收到header后首先需要判断magic_num/checksum是否正确,
以确定这个packet是否合法。如果不合法,就以max_buff的大小来收,由于是nonblock,所以以最大的包来收,没数据会立即返回,
然后continue. 对于sequence num discontiguous packets,do some process.
如:
/*------------------------------------------------------------------------*/ if((message->magic_num != MAGIC_NUMBER ) || (message->header_len != sizeof(PHS))){ MY_ERROR("-----------------wrong message header ,drop it ! ------------------------/n"); ret = read(sock,sendbuf,256*1024);//nonblock ,return as soon as possible ret = 0; usleep(10); continue; } /*------------------------------------------------------------------------*/
10>严格判断函数的返回值,以确定下一步的操作。

11>如果程序出现问题,需要从最开始的源头寻找问题的来源,而不要盲目的脚痛医脚。

12> 使用signal(SIGPIPE, SIG_IGN); 忽略掉socket中断产生的SIGPIPE信号,以致于程序不被异常中断。

13>在write/send的时候,也需要先FD_ISSET() -> rc = read()/recv -> if(rc == 0)则说明收到FIN/RST,socket中断,无法恢复,做清理工作 。
if(rc < 0)且EAGAIN == errno,则socket出错,无法恢复,做清理工作。如 <19>实例所示.


14>适当设置线程的优先级,以达到最重要的线程可以顺畅运行,而不至于时常被打断。

15>不要出现无退出条件的while(1),for(;;)之类的,因为任何一个程序,都有可能被中断,
一旦收到中断信号,线程就需要一个退出条件,while(!flag),当收到某个中断信号后,就需要执行某个函数,在这个函数中做一些清理工作,包括flag = 1;
如:
/*------------------------------------------------------------------------*/ signal(SIGINT, sigchld_handler); signal(SIGTERM, sigchld_handler); signal(SIGPIPE, SIG_IGN); // ignore SIGPIPE void sigchld_handler(void *s) { printf("___Interrupt,Clean Up done Quit.../n"); flag = 1; } /*------------------------------------------------------------------------*/


16>在使用signal(SIGPIPE, SIG_IGN);后程序还是会收到Interrupted system call信号,所以在select处,需要catch这个信号,然后做完一些清理工作再退出。
如下:
/*------------------------------------------------------------------------*/ for(;;) { //the following codes will break out,only just when (errno == EINTR) ,loop will re-run,so it doesn't matter. rc = select(sockfd+1, &rset, (fd_set *)NULL, (fd_set *)NULL, &Timeout); if(rc < 0) { if(errno == EINTR){ printf("catch a Interrupted system call,Insist on cleaning work .../n"); continue; } fprintf(stderr, "select() error: %s/n", strerror(errno)); return FALSE; //or goto _EXIT_CLEANWORK_; } else if(rc == 0) { printf("select() timeout .../n"); return FALSE; //or continue; } } /*-------------------------------------------------------------------------*/
17>尽量使函数的功能简单,一个函数只干一件事,对于功能类似的操作,尽量封装到同一函数中。

18>在写socket操作前,先判断该socket是否可读,以确认该socket是否已被关闭。

19>我自己一般读socket方法如下:
eg:
/*-------------------------------------------------------------------------*/ int read_sock (int sockhandle, unsigned char *buf, int length) { int byte_read = -1; unsigned char *ptbuf =buf; int mlength = 0; int i = 0; fd_set rset; struct timeval timeout; int rc; int retrytime = 2; if(length > 1000){ retrytime = 10; } do { if((byte_read <= 0) && (i++ > retrytime )) return mlength; FD_ZERO(&rset); FD_SET(sockhandle,&rset); timeout.tv_sec = 1; timeout.tv_usec = 0; byte_read = 0; rc = select(sockhandle+1,&rset,NULL,NULL,&timeout); if(rc < 0){ if(errno == EINTR){ printf("catch a Interrupted system call,Insist on cleaning work .../n"); continue; } perror("select() error"); return -1; } else if(rc == 0){ //MY_DEBUG("select timeout/n"); usleep(100); continue; } rc = 0; if(FD_ISSET(sockhandle,&rset)){ byte_read = read (sockhandle, ptbuf,length-mlength); if(byte_read < 0){ if(errno == EAGAIN){ usleep(10); continue; } perror("socket recv error"); return -1; } else if(byte_read == 0){ printf("socket recv FIN/RST /n"); return -1; } else{ ptbuf = ptbuf+byte_read; mlength = mlength+byte_read; //printf("reste to read %d /n",mlength); } } } while (mlength < length); return (mlength); } /*-------------------------------------------------------------------------*/
20>函数的返回值,如果定义为unsigned 类型的,对这个函数的返回值判断一定要谨慎,不能以 <0 或者 >0为比较条件,因为它是unsigned,必然大于0。
如:
/*--return value test--*/ unsigned long function_test(...){ //waiting for your performace //eg: if(x){ return 1; } else if(y){ return -1; } else{ return 0; } } /*--caller--*/ int ret ; ret = function_test(...); //if(ret < 0){ //wrong ! if(ret == -1){ ... } else if(ret == 0 ){ ... } else{ ... }
21> 尽量少用malloc,因为一旦使用了malloc,就注定着你要为它安排一个同伴free(),而有时候你收到了malloc的“好处”就忘记了给它安排完整的free(),
程序任何一个走向都将要给它安排同伴,你逃是逃不掉的。
一旦使用了malloc,也注定了你必然要判断一下它是否是活的,谁都不愿意为死人效劳,因为死人无法给自己带来好处。
eg:

unsigned char * ptr = NULL; ptr = (unsigned char *)malloc(sizeof(PHS)); if(ptr == NULL){ return _ERROR_MALLOC_; } //your performace place ... free(ptr); ptr = NULL;//some people don't like give NULL to the pointer,but some other people like this.
22>当一个listen主进程或者主线程得到一个连接就开一个线程处理时,这个新开的线程中的while循环条件与前面的线程ID一样,
都需要是这个线程独有的,不会影响到其它并发线程,否则,所有线程都将会因为一个线程的坏掉而都死掉。

 

23>write/recv之前,对方socket中断,write/recv会先调用SIGPIPE响应函数,由于将SIGPIPE交给了系统,则write/recv会返回-1,errno号为EPIPE(32).

 

socket write/recv过程中,对方socket中断,write/recv会先返回已经发送的字节数,再次write时返回-1,errno号为ECONNRESET(104).
即:write/recv 一个已收到RST的socket,系统会发SIGPIPE信号给该进程,如果将这个信号交给系统处理或者直接忽略掉了,write/recv都返回EPIPE错误.
因此对于socket通信一定要捕获此信号,进行适当处理 ,否则程序的异常退出将会给你带来灾难。
参考:
The client's call to readline may happen before the server's RST is received by the client, or it may happen after. 
If the readline happens before the RST is received, as we've shown in our example, the result is an unexpected EOF in the client. 
But if the RST arrives first, the result is an ECONNRESET ("Connection reset by peer") error return from readline.
What happens if the client ignores the error return from readline and writes more data to the server? 
This can happen, for example, if the client needs to perform two writes to the server before reading anything back, 
with the first write eliciting the RST.
The rule that applies is: When a process writes to a socket that has received an RST, the SIGPIPE signal is sent to the process. 
The default action of this signal is to terminate the process, so the process must catch the signal to avoid being involuntarily terminated.
If the process either catches the signal and returns from the signal handler, or ignores the signal, the write operation returns EPIPE. 

你可能感兴趣的:(C,C++,Linux系统编程)