基本套接字編程(6) -- 線程篇

1. 線程

傳統Unix模型中,當一個進程須要另外一個實體來完成某事,它就fork一個子進程來處理。Unix上大多數網絡服務器程序即是以建立多個子進程的方式實現的:父進程accept一個鏈接,fork一個子進程,該子進程處理與該鏈接對端的客戶之間的通訊。
儘管,這種範式多年來一直用的不錯,可是fork調用依然存在一些問題:
(1)fork是昂貴的,fork要把父進程的內存影像複製到子進程,並在子進程中複製全部描述符,等等。當今的實現使用寫時複製技術,用於避免在子進程切實須要本身的副本以前把父進程的數據空間複製到子進程。然而,即便有這樣的優點,fork仍然是昂貴的;
(2)fork返回以後,父子進程之間信息的傳遞須要進程間通訊(IPC)機制。調用fork以前父進程向還沒有存在的子進程傳遞信息至關容易,由於子進程將從父進程數據空間及全部描述符的一個副本開始進程,然而從子進程向父進程返回信息缺很是吃力;
針對以上的問題,線程有助於解決,線程也稱爲輕量級進程。

1.1 線程共享信息

同一進程內的全部線程共享如下信息:
  • 相同的全局內存,包含全局變量
  • 進程指令
  • 大多數數據
  • 打開的文件(即描述符)
  • 信號處理函數和信號處置
  • 當前工做目錄
  • 用戶ID和組ID

1.2 線程獨立信息

  • 線程ID
  • 寄存器集合,包括程序計數器和棧指針
  • 棧(用於存放局部變量和返回地址)
  • errno
  • 信號掩碼
  • 優先級
本文總結於《Unix網絡編程 -- 卷一》第26章,講解的爲POSIX線程也稱爲pthread。

2. 基本線程函數

Pthreads API在ANSI/IEEE POSIX 1003.1 – 1995標準中定義。不像MPI,該標準不是免費的,必須向IEEE購買。 
Pthreads API中的函數能夠非正式的劃分爲三大類: 
  • 線程管理(Thread management): 第一類函數直接用於線程:建立(creating),分離(detaching),鏈接(joining)等等。包含了用於設置和查詢線程屬性(可鏈接,調度屬性等)的函數。
  • 互斥量(Mutexes): 第二類函數是用於線程同步的,稱爲互斥量(mutexes),是"mutual exclusion"的縮寫。Mutex函數提供了建立,銷燬,鎖定和解鎖互斥量的功能。同時還包括了一些用於設定或修改互斥量屬性的函數。 
  • 條件變量(Condition variables):第三類函數處理共享一個互斥量的線程間的通訊,基於程序員指定的條件。這類函數包括指定的條件變量的建立,銷燬,等待和受信(signal)。設置查詢條件變量屬性的函數也包含其中。 
命名約定:線程庫中的全部標識符都以pthread開頭 

2.1 線程建立 -- pthread_create函數

最初,main函數包含了一個缺省的線程。其它線程則須要程序員顯式地建立。 pthread_create 建立一個新線程並使之運行起來。該函數能夠在程序的任何地方調用。 
#include <pthread.h>
int pthread_create(pthread_t *tid , const pthread_attr_t *attr , void *(*func)(void *) , void *arg);
<span style="white-space:pre">							</span>返回:若成功則爲0 , 若出錯則爲正的Exxx值

 pthread_create參數: git

  • tid:返回一個不透明的,惟一的新線程標識符。 
  • attr:不透明的線程屬性對象。能夠指定一個線程屬性對象,或者NULL爲缺省值。 
  • func:線程將會執行一次的C函數。 
  • arg: 傳遞給func單個參數,傳遞時必須轉換成指向void的指針類型。沒有參數傳遞時,可設置爲NULL。 
一個進程能夠建立的線程最大數量取決於系統實現。 一旦建立,線程就稱爲peers,能夠建立其它線程。線程之間沒有指定的結構和依賴關係。 
注:
Q:一個線程被建立後,怎麼知道操做系統什麼時候調度該線程使之運行? 
A:除非使用了Pthreads的調度機制,不然線程什麼時候何地被執行取決於操做系統的實現。強壯的程序應該不依賴於線程執行的順序。

2.2 線程匯合 -- pthread_join函數

咱們能夠經過調用pthread_join等待一個給定線程終止。對比線程和Unix進程,pthread_create相似於fork,pthread_join相似於waitpid。
#include <pthread.h>
int pthread_join(pthead_t *tid , void **status);
						返回:若成功則爲0,若出錯則爲正的Exxx值
咱們必須指定要等待線程的tid,Pthread沒辦法等待任意一個線程(相似指定waitpid進程Id參數-1)。
若是status指針非空,來自所等待線程的返回值(一個指向某個對象的指針)將存入由status指向的位置。

2.3 線程分離 -- pthread_detach函數

一個線程是可匯合的(joinable默認值)或者是脫離的(detached)。當一個可匯合的線程終止時,它的線程ID和退出狀態將留存到另外一個線程對它調用pthread_join。脫離的線程卻像守護進程,當它們終止時,全部相關資源都被釋放,咱們不能等待它們終止。若是一個線程須要知道另外一個線程何時終止,那就最好保持第二個線程的可匯合狀態。
pthread_detach函數將指定的線程轉變爲脫離狀態。
#include <pthread.h>
int pthread_detach(pthead_t *tid);
						返回:若成功則爲0,若出錯則爲正的Exxx值

若是一個線程想讓自身脫離,則調用:pthread_detach(pthread_self());程序員

注:線程匯合與分離

(1)鏈接:  「鏈接」是一種在線程間完成同步的方法。 

  • pthread_join()函數阻賽調用線程知道threadid所指定的線程終止。 
  • 若是在目標線程中調用pthread_exit(),程序員能夠在主線程中得到目標線程的終止狀態。 
  • 鏈接線程只能用pthread_join()鏈接一次。若屢次調用就會發生邏輯錯誤。 
  • 兩種同步方法,互斥量(mutexes)和條件變量(condition variables)。 
(2)可鏈接(Joinable or Not)? 
當一個線程被建立,它有一個屬性定義了它是可鏈接的(joinable)仍是分離的(detached)。只有是可鏈接的線程才能被鏈接(joined),若果建立的線程是分離的,則不能鏈接。 POSIX標準的最終草案指定了線程必須建立成可鏈接的。然而,並不是全部實現都遵循此約定。 
使用pthread_create()的attr參數能夠顯式的建立可鏈接或分離的線程,典型四步以下: 
  • 聲明一個pthread_attr_t數據類型的線程屬性變量 
  • 用 pthread_attr_init()初始化改屬性變量 
  • 用pthread_attr_setdetachstate()設置可分離狀態屬性 
  • 完了後,用pthread_attr_destroy()釋放屬性所佔用的庫資源 
(3)分離(Detaching): 
  • pthread_detach()能夠顯式用於分離線程,儘管建立時是可鏈接的。
  • 沒有與pthread_detach()功能相反的函數 。
(4)建議: 
  • 若線程須要鏈接,考慮建立時顯式設置爲可鏈接的。由於並不是全部建立線程的實現都是將線程建立爲可鏈接的。 
  • 若事先知道線程從不須要鏈接,考慮建立線程時將其設置爲可分離狀態。一些系統資源可能須要釋放。 

2.4 線程ID -- pthread_self函數

每個線程都有一個所屬進程內標識自身的ID。線程ID由pthread_create返回,pthread_join使用它,而函數pthread_self則是獲取自身的線程ID。
#include <pthread.h>
int pthread_self(void);                                   返回:調用線程的ID            

  

2.5 線程退出 -- pthread_exit函數

讓一個線程終止的方法之一是調用pthread_exit函數:
#include <pthread.h>
void pthread_self(void *status);
						不返回到調用者
若是本線程不曾脫離,它的線程ID和退出狀態將一直留存到調用進程內的某個其它線程對它調用pthread_join。
指針status不能指向局部於調用線程的對象,由於線程終止時這樣的對象也消失。
讓一個線程終止的另外兩個方法:
  • 啓動線程的函數(即pthread_create()的第三個參數)能夠返回。既然該函數必須聲明成返回一個void指針,它的返回值就是相應線程的終止狀態。
  • 若是進程的main函數返回或者任何線程調用了exit,整個進程就終止,其中包括它的任何線程。

3. 多線程TCP客戶/服務器聊天程序實例

上面實例介紹了利用多線程技術實現的TCP客戶/服務器回射程序實例,下面介紹利用多線程技術實現TCP客戶端/服務器聊天小程序。
該程序實現客戶端與服務器端通訊,當任意一端發送退出指令exit時,程序結束。

3.1 config.h

 1 /*
 2  * config.h 包含該tcp/ip套接字編程所須要的基本頭文件,與server.c client.c位於同一目錄下
 3 */
 4 
 5 #include <stdio.h>
 6 #include <stdlib.h>
 7 #include <string.h>
 8 #include <sys/socket.h>
 9 #include <sys/types.h>
10 #include <unistd.h>
11 #include <errno.h>
12 #include <netinet/in.h>
13 #include <netdb.h>
14 #include <arpa/inet.h>
15 #include <pthread.h>
16 
17 const int MAX_LINE = 2048;
18 const int PORT = 6001;
19 const int BACKLOG = 10;
20 const int LISTENQ = 6666;
21 const int MAX_CONNECT = 20;

3.2 server.c

  1 /*
  2 *  服務器端代碼實現
  3 */
  4 
  5 #include "config.h"
  6 
  7 /*處理接收客戶端消息函數*/
  8 void *recv_message(void *fd)
  9 {
 10     int sockfd = *(int *)fd;
 11     while(1)
 12     {
 13         char buf[MAX_LINE];
 14         memset(buf , 0 , MAX_LINE);
 15         int n;
 16         if((n = recv(sockfd , buf , MAX_LINE , 0)) == -1)
 17         {
 18             perror("recv error.\n");
 19             exit(1);
 20         }//if
 21         buf[n] = '\0';        
 22         //若收到的是exit字符,則表明退出通訊
 23         if(strcmp(buf , "byebye.") == 0)
 24         {
 25             printf("Client closed.\n");
 26             close(sockfd);
 27             exit(1);
 28         }//if
 29 
 30         printf("\nClient: %s\n", buf);
 31     }//while
 32 }
 33 
 34 int main()
 35 {
 36 
 37     //聲明套接字
 38     int listenfd , connfd;
 39     socklen_t clilen;
 40     //聲明線程ID
 41     pthread_t recv_tid , send_tid;
 42 
 43     //定義地址結構
 44     struct sockaddr_in servaddr , cliaddr;
 45     
 46     /*(1) 建立套接字*/
 47     if((listenfd = socket(AF_INET , SOCK_STREAM , 0)) == -1)
 48     {
 49         perror("socket error.\n");
 50         exit(1);
 51     }//if
 52 
 53     /*(2) 初始化地址結構*/
 54     bzero(&servaddr , sizeof(servaddr));
 55     servaddr.sin_family = AF_INET;
 56     servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
 57     servaddr.sin_port = htons(PORT);
 58 
 59     /*(3) 綁定套接字和端口*/
 60     if(bind(listenfd , (struct sockaddr *)&servaddr , sizeof(servaddr)) < 0)
 61     {
 62         perror("bind error.\n");
 63         exit(1);
 64     }//if
 65 
 66     /*(4) 監聽*/
 67     if(listen(listenfd , LISTENQ) < 0)
 68     {
 69         perror("listen error.\n");
 70         exit(1);
 71     }//if
 72 
 73     /*(5) 接受客戶請求,並建立線程處理*/
 74 
 75     clilen = sizeof(cliaddr);
 76     if((connfd = accept(listenfd , (struct sockaddr *)&cliaddr , &clilen)) < 0)
 77     {
 78         perror("accept error.\n");
 79         exit(1);
 80     }//if
 81 
 82     printf("server: got connection from %s\n", inet_ntoa(cliaddr.sin_addr));
 83 
 84     /*建立子線程處理該客戶連接接收消息*/
 85     if(pthread_create(&recv_tid , NULL , recv_message, &connfd) == -1)
 86     {
 87         perror("pthread create error.\n");
 88         exit(1);
 89     }//if
 90 
 91     /*處理服務器發送消息*/
 92     char msg[MAX_LINE];
 93     memset(msg , 0 , MAX_LINE);
 94     while(fgets(msg , MAX_LINE , stdin) != NULL)    
 95     {    
 96         if(strcmp(msg , "exit\n") == 0)
 97         {
 98             printf("byebye.\n");
 99             memset(msg , 0 , MAX_LINE);
100             strcpy(msg , "byebye.");
101             send(connfd , msg , strlen(msg) , 0);
102             close(connfd);
103             exit(0);
104         }//if
105 
106         if(send(connfd , msg , strlen(msg) , 0) == -1)
107         {
108             perror("send error.\n");
109             exit(1);
110         }//if        
111     }//while
112 }

 

3.3 client.c

  1 /*
  2 * 客戶端代碼
  3 */
  4 #include "config.h"
  5 
  6 /*處理接收服務器消息函數*/
  7 void *recv_message(void *fd)
  8 {
  9     int sockfd = *(int *)fd;
 10     while(1)
 11     {
 12         char buf[MAX_LINE];
 13         memset(buf , 0 , MAX_LINE);
 14         int n;
 15         if((n = recv(sockfd , buf , MAX_LINE , 0)) == -1)
 16         {
 17             perror("recv error.\n");
 18             exit(1);
 19         }//if
 20         buf[n] = '\0';
 21         
 22         //若收到的是exit字符,則表明退出通訊
 23         if(strcmp(buf , "byebye.") == 0)
 24         {
 25             printf("Server is closed.\n");
 26             close(sockfd);
 27             exit(0);
 28         }//if
 29 
 30         printf("\nServer: %s\n", buf);
 31     }//while
 32 }
 33 
 34 
 35 int main(int argc , char **argv)
 36 {
 37     /*聲明套接字和連接服務器地址*/
 38     int sockfd;
 39     pthread_t recv_tid , send_tid;
 40     struct sockaddr_in servaddr;
 41 
 42     /*判斷是否爲合法輸入*/
 43     if(argc != 2)
 44     {
 45         perror("usage:tcpcli <IPaddress>");
 46         exit(1);
 47     }//if
 48 
 49     /*(1) 建立套接字*/
 50     if((sockfd = socket(AF_INET , SOCK_STREAM , 0)) == -1)
 51     {
 52         perror("socket error");
 53         exit(1);
 54     }//if
 55 
 56     /*(2) 設置連接服務器地址結構*/
 57     bzero(&servaddr , sizeof(servaddr));
 58     servaddr.sin_family = AF_INET;
 59     servaddr.sin_port = htons(PORT);
 60     if(inet_pton(AF_INET , argv[1] , &servaddr.sin_addr) < 0)
 61     {
 62         printf("inet_pton error for %s\n",argv[1]);
 63         exit(1);
 64     }//if
 65 
 66     /*(3) 發送連接服務器請求*/
 67     if( connect(sockfd , (struct sockaddr *)&servaddr , sizeof(servaddr)) < 0)
 68     {
 69         perror("connect error");
 70         exit(1);
 71     }//if    
 72 
 73     /*建立子線程處理該客戶連接接收消息*/
 74     if(pthread_create(&recv_tid , NULL , recv_message, &sockfd) == -1)
 75     {
 76         perror("pthread create error.\n");
 77         exit(1);
 78     }//if    
 79 
 80     /*處理客戶端發送消息*/
 81     char msg[MAX_LINE];
 82     memset(msg , 0 , MAX_LINE);
 83     while(fgets(msg , MAX_LINE , stdin) != NULL)    
 84     {
 85         if(strcmp(msg , "exit\n") == 0)
 86         {
 87             printf("byebye.\n");
 88             memset(msg , 0 , MAX_LINE);
 89             strcpy(msg , "byebye.");
 90             send(sockfd , msg , strlen(msg) , 0);
 91             close(sockfd);
 92             exit(0);
 93         }//if
 94         if(send(sockfd , msg , strlen(msg) , 0) == -1)
 95         {
 96             perror("send error.\n");
 97             exit(1);
 98         }//if
 99     
100         
101     }//while
102 }

 

4.4 運行結果

服務器端:
客戶端:



GitHub源碼網址
相關文章
相關標籤/搜索