如何写一个完美的socket多线程通信程序 ?(持续更新中...)
/*
* 由socket多线程通信中一些常见问题与技巧引发的一系列程序开发秘籍,(个人经验,仅供参考)
* 请结合http://blog.youkuaiyun.com/ipromiseu/archive/2009/05/12/4169589.aspx《Linux系统下的多线程编程全面入门 》阅读本文
* by . Gray Luo(guohui.great@gmail.com)
*/
1.socket多线程通信中一些常见问题与技巧:
1>socket考虑到效率,一般采用nonblock方式。
2>socket通信或者其它读写操作,使用select进行可读写的检测,一方面可以防止阻塞占用CPU,另一方面可以安全地进行数据读写。
3>read/write 或 recv/send 操作之前都需要使用FD_ISSET()检测句柄是否可读写。
4>read/write 或 recv/send 都需要重新封装以达到对数据的完整收发与出错处理。 请参考我的另一篇文章《通信中如何一次性完整地接收数据》http://blog.youkuaiyun.com/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).