1 前言
2 Socket編程
2.1 Socket通信機制
2.2 socket通信示例圖
2.3 Socket在不同平臺上的實現
2.3.1 Socket在Windows平臺中的實現
2.3.2 Socket在Linux/Unix平臺中的實現
2.3.3 可移植的啟動和結束調用代碼
2.3.4 其它移植問題
3 多線程編程
3.1 線程與進程的不同
3.2 線程沖突與數據保護
3.3 Win32中的線程
3.3.1 線程同步
3.3.2 創建線程
3.4 Linux/Unix中的線程
3.5 可移植的線程代碼
4 程序實例
前言
Socket編程特別是多線程編程是一個很大的課題,本文針對公司最近將要實現的下載版和網頁版的CPR和CPE兩個軟件來講解socket和多線程的可移植編程技術,所涉及的是其中較基礎的部分,以盡量滿足當前公司需要為準。
Socket編程
Socket通信機制通過socket可實現點對點或廣播通信程序,兩者之間的區別不大,編程時其程序流程所用代碼幾乎相同,不同的地方在于目標地址選擇的不同。本教材所舉實例為點對點的形式,即以客戶/服務器形式來實現一個socket通信程序,為描述方便,我們對兩點分別命名為Server和Client。Socket實現Server 與Client之間的通信的過程,非常象電信局的普通電話服務,為了更好的理解socket,以下就以電信局的電話服務作為類比來說明socket的通信機制:
* 電信局提供電話服務類似我們這的Server,普通電話用戶類似我們這的Client。
* 首先電信局必須建立一個電話總機。這就如果我們必須在Server端建立一個Socket(套接字),這一步通過調用socket()函數實現。
* 電信局必須給電話總機分配一個號碼,以便使用戶要撥找該號碼得到電話服務,同時接入該電信局的用戶必須知道該總機的號碼。同樣,我也在Server端也要為這一套接字指定一port(端口),并且要連接該Server的Client必須知道該端口。這一步通過調用bind()函數實現。
* 接下來電信局必須使總機開通并使總機能夠高效地監聽用戶撥號,如果電信局所提供服務的用戶數太多,你會發現撥打電信局總機老是忙音,通常電信局內部會使該總機對應的電話號碼連到好幾個負責交換的處理中心,在一個處理中心忙于處理當前的某個用戶時,新到用戶可自動轉到一下處理中心得到服務。同樣我們的Server端也要使自己的套接口設置成監聽狀態,這是通用listen()函數實現的,listen()的第二個參數是等待隊列數,就如同你可以指定電信局的建立幾個負責交換的處理中心。
* 用戶知道了電信局的總機號后就可以進行撥打請求得到服務。在Winsock的世界里做為Client端是要先用socket()函數建立一個套接字,然后調connect()函數進行連接。當然和電話一樣,如果等待隊列數滿了、與Server的線路不通或是Server沒有提供此項服務時,連接就不會成功。
* 電信局的總機接受了這用戶撥打的電話后負責接通用戶的線路,而總機本身則再回到等待的狀態。Server也是一樣,調用accept()函數進入監聽處理過程,Server端的代碼即在中處暫停,一旦Server端接到申請后系統會建立一個新的套接字來對此連接做服務,而原先的套接字則再回到監聽等待的狀態。
* 當你電話掛完了,你就可以掛上電話,彼此間也就離線了。Client和Server間的套接字的關閉也是如此;這個關閉離線的動作,可由Client端或Server端任一方先關閉,這也與電話局的服務類似。
從以上情況可以看出在服務器端建立一個套接字,進入監聽狀態到接收到一個客戶端的請求的過程如下:
socket()->bind()->listen()->accept()->recv()->send()
在客戶端則要簡單得多,其調用過程如下:
socket()->connect()->send()->recv()
socket通信示例圖
下圖顯示的通信方式用在許多場合,比如HTTP協議中用的就是這種方式。FTP、TELNET等協議也與此大致相同,不同之外在于FTP、TELNET這類協議會多次重復send()和recv()的調用。
Socket在不同平臺上的實現
Socket在Windows平臺中的實現
Socket在Windows中的實現稱為Winsock,核心文件是winsock.dll(或winsock32.dll)。這個動態鏈接庫負責提供標準的socket調用API和其它幾個特定的Windows平臺專用API,另外它還實現了適用于Windows消息機制的同步socket通信機制。在Winsock中所有非標準的socket調用均以WSA三個字母開頭命名。
由于Win32的各個平臺均支持線程,我采用同步socket通信機制來編寫Winsock程序不可取。一是多線程的系統中對同步通信有更好的解決辦法,二是同步通信機制程序沒有很好的兼容性。
Winsock中有兩個比較重要的函數就是WSAStartup()和WSACleanup(),它們的作用在于對Winsock動態鏈接庫進行一些初始化或清除工作。在一個進程中,沒有調用WSAStartup()之前將無法正確調用任何其它標準的socket函數。
Socket在Linux/Unix平臺中的實現
Socket最早是在Unix平臺上實現的,socket調用已是當今Unix平臺的標準系統調用。Linux的網絡部分模塊幾乎是原封不動的從Unix中遷移過來的,其函數的調用方式基本與Unix相同。
在Linux/Unix平臺中,如果你正在向一個套接字發信息,而遠端計算機又關閉了該端口,將產生一個SIGPIPE信號,該信號的默認處理過程將中止當前進程,為使我們的socket程序能夠健壯的運行,必須接管該信息的處理過程,一般情況使系統忽略該信號即可。
可移植的啟動和結束調用代碼
根據以上兩點所述的socket中不同平臺中的實現方式,可以編寫出可移植的socket初始化和結束調用代碼如下:
bool bsocket_init() //初始化socket 調用
{
#ifdef _WIN32
WSADATA wsad;
WORD wVersionReq;
wVersionReq=MAKEWORD(1,1); //清求不低于1.1版本的Winsock
if(WSAStartup(wVersionReq,&wsad))
return false;
return true;
#else
signal(SIGPIPE, SIG_IGN); //忽略SIGPIPE信號
return true;
#endif
}
void bsocket_cleanup() //結束socket 調用
{
#ifdef _WIN32
WSACleanup();
#else
signal(SIGPIPE, SIG_DFL); //恢復SIGPIPE信號
return;
#endif
}
其它移植問題
關閉socket
Windows中關閉socket的調用為closesocket(socket sock_in); Linux為close(socket sock_in);
Socket地址長度類型
Socket的許多調用中均用到socket地址長度做為參數,該參數在Windows中為int類型,在Linux中為socklen_t類型。
無效的socket
Windows中調用socket()和accept()時,如果返回的是無效的socket,則該值為INVALID_SOCKET,Linux中為-1。
調用錯誤返回
Windows中除上述的socket()和accept()以及幾個少數的調用外,大部分調用在出錯時返回值為SOCKET_ERROR,Linux中為-1。
多線程編程
線程與進程的不同
當把一個可執行程序裝載在內存中后,不管你是你是準備它或是已經執行它或是被暫停執行,它都被稱為一個進程。你可以稱一個進程為一個可執行程序在內存中的實例。雖然一個進程可以復制自己,也可以調用其它進程或與其它進程通信,全基本上進程是由操作系統直接管理的資源。每一個進程均有一個與其它進程獨立的操作系統運行環境,比如環境變量、當前目錄等。
線程是一種可執行體,它表示對CPU運行時間的占有權,線程也是操作系統中的一種資源,它的創建、中止最終要由操作系統來實現。但線程每一個線程更多表現為由一個進程直接來管理。每一個線程均有一個與其它線程不同的硬件運行環境,比如各CPU中通用寄存器和狀態寄存器的值。
一個進程的創建需要操作系統做大量工作,如設置環境變量、裝裁可執行代碼、分配進程資源等,進程間的切換也需做大量工作。而線程的創建的切換相對來說要簡單得多,操作也能夠以最快的速度來實現不同執行體之間的切換。
另一方面,由于所有線程共享一個進程空間,線程間的通信變得十分簡單;而進程之間由于各自內存相互隔離,進程之間通信只能通過管道、發送系統消息或信號來實現。
線程沖突與數據保護
由于同一進程中的線程由操作系統并發執行。當一個進程中的許多線程要修改某些公用變量時就存在數據保護問題。解決這個問題的通常辦法是向操作系統申請一個線程同步對象,操作系統只允許同時有一個少數個線程訪問受保護的數據。
Win32中的線程
線程同步
Win32中可用于線程同步的對象有CRITICAL_SECTION, Event, Mutex, Semaphore和Waitable timer等。 其中CRITICAL_SECTION比較適于解決在單一進程多線程的沖突問題。
與CRITICAL_SECTION有關的幾個函數為:
* InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
該函數初始化一個CRITICAL_SECTION。
* DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
該函數清除一個CRITICAL_SECTION。
* EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
該函數使本線程取得CRITICAL_SECTION控制權,代碼進入保護狀態。
* LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
該函數使本線程放棄CRITICAL_SECTION控制權,代碼退出保護狀態。
創建線程
Win32中最常用的創建線程的函數為CreateThread(),該函數的格式為:
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
DWORD dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId);
* lpThreadAttributes: 輸入參數。指定是否本次函數返回的線程句柄可以被子進程繼承,可以為NULL。在CPR/CPE兩個產品下載版和網頁版的開發過程代碼使用NULL應可滿足需要。
* dwStackSize : 輸入參數。指定指定的線程堆棧大小,可以為0,如果為0,使用默認堆棧大小。在CPR/CPE兩個產品下載版和網頁版的開發過程代碼使用0應可滿足需要。
* lpStartAddress : 輸入參數。指向線程函數,該函數包含線程的執行代碼。
* lpParameter : 輸入參數。指定要傳給線程的參數塊內存地址。
* lpThreadId : 輸出參數。該參數在調用結束后將包含被創建線程的ID。
Linux/Unix中的線程
當前各版本的Linux和Unix本身并不支持線程,但在Linux/Unix各版本均自帶pthread線程包及它們的開發運行頭文件和庫文件。
與Win32類似,pthread包中用于線程同步的對象也有多種,在我們的開發應用中,使用pthread_mutex_t即可滿足需求。與pthread_mutex_t相關的幾個函數為:
*
pthread_mutex_init(pthread_mutex_t * p, pthread_mutexattr_t * pa);
該函數初始化一個pthread_mutex_t,其中pa參數可以設置pthread_mutex_t的屬性。與Win32中多線程的機制稍有不同,在默認情況下,pthread中對同一線程的多次保護請求會造成互鎖,以我們產品開發的情況來看,要修改pthread的屬性方可滿足實際需要,但pthread中設置多線程同步屬性的方法的可移植性不高。對同一線程多次申請保護的問題可以記錄線程ID的辦法來解決。
* pthread_mutex_destroy(pthread_mutex_t * p);
清除一個已初始化的pthread_mutex_t。
* pthread_mutex_lock(pthread_mutex_t * p);
請求取得pthread_mutex_t控制權,進入代碼保護。
* pthread_mutex_unlock(pthread_mutex_t * p);
放棄一個pthread_mutex_t控制權,退出代碼保護。
可移植的線程代碼
(參見程序實例)
程序實例
#include
#ifdef _WIN32
#include
#define socklen_t int
#else
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define closesocket(_x) close(_x)
#endif
#ifdef _WIN32
#define THREAD_PROC WINAPI
#define THREAD_RETURN DWORD
#define THREAD_PARAM void *
#define THREAD_ID DWORD
#define CPO CRITICAL_SECTION
#define CPO_Init(_x) InitializeCriticalSection(&_x)
#define CPO_Dele(_x) DeleteCriticalSection(&_x)
#define CPO_Enter(_x) EnterCriticalSection(&_x)
#define CPO_Leave(_x) LeaveCriticalSection(&_x)
#else
struct CPO { pthread_mutex_t m; pthread_t t;};
#define THREAD_PROC
#define THREAD_RETURN void *
#define THREAD_PARAM void *
#define THREAD_ID pthread_t
#define CPO_Init(_x) { _x.t=0; pthread_mutex_init(&_x.m, NULL); }
#define CPO_Dele(_x) { pthread_mutex_destroy(&_x.m); }
#define CPO_Enter(_x) while(true) \
{ \
if(_x.t==0) \
{ \
pthread_mutex_lock(&_x.m); \
_x.t=pthread_self(); \
break;\
}\
if(pthread_self()==_x.t)\
break; \
pthread_mutex_lock(&_x.m); \
_x.t=pthread_self();\
break; \
}
#define CPO_Leave(_x) { pthread_mutex_unlock(&_x.m); _x.t=0; }
#endif
typedef THREAD_RETURN THREAD_PROC THREAD_FUNCTION(THREAD_PARAM thread_param);
#ifdef _WIN32
THREAD_ID bcreate_thread(THREAD_FUNCTION pfun, void * pparam)
{
THREAD_ID tid;
if(CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)pfun, (LPVOID)pparam, 0, &tid)==NULL)
return (THREAD_ID)-1;
return tid;
}
#else
THREAD_ID bcreate_thread(THREAD_FUNCTION pfun, void * pparam)
{
THREAD_ID tid;
if(pthread_create(&tid, NULL, (void * (*)(void *))pfun, pparam)<1)
return (THREAD_ID)-1;
pthread_detach(tid);
return tid;
}
#endif
#ifndef SOCKET
#define SOCKET int
#endif
#ifndef SOCKET_ERROR
#define SOCKET_ERROR -1
#endif
#ifndef INVALID_SOCKET
#define INVALID_SOCKET -1
#endif
bool bsocket_init()
{
#ifdef _WIN32
WSADATA wsad;
WORD wVersionReq;
wVersionReq=MAKEWORD(1,1);
if(WSAStartup(wVersionReq,&wsad))
return false;
return true;
#else
signal(SIGPIPE, SIG_IGN);
return true;
#endif
}
void bsocket_cleanup()
{
#ifdef _WIN32
WSACleanup();
#else
signal(SIGPIPE, SIG_DFL);
return;
#endif
}
#define WEB_SERVER_PORT 90
#define WEB_SERVER_IP "127.0.0.1"
#define HTTP_HEAD "HTTP/1.0 200 OK\x0d\x0a""Content-type: text/html\x0d\x0a\x0d\x0a"
#define WELCOME_HTML "\n\nWelcome to my Website!
\ng_hits: %d\n\n\n"
CPO g_cpo;
int g_hits;
THREAD_RETURN THREAD_PROC do_accept(THREAD_PARAM param)
{
SOCKET sock=((SOCKET *)param)[0];
char ptmp[2048];
recv(sock, ptmp, 2048, 0);
CPO_Enter(g_cpo);
int a=g_hits;
sprintf(ptmp, WELCOME_HTML, a++);
g_hits=a;
CPO_Leave(g_cpo);
send(sock, HTTP_HEAD, strlen(HTTP_HEAD), 0);
send(sock, ptmp, strlen(ptmp), 0);
closesocket(sock);
return 0;
}
main(int argc,char ** argv)
{
char perror[1024];
SOCKET s,rs;
sockaddr_in sin,rsin;
socklen_t slen;
bool result=false;
perror[0]=0;
CPO_Init(g_cpo);
g_hits=0;
if(!bsocket_init())
{
strcpy(perror, "Can't init socket!");
goto error_out;
}
s=socket(PF_INET,SOCK_STREAM,0);
if(s==INVALID_SOCKET)
{
strcpy(perror, "Can't create socket!");
goto error_out;
}
sin.sin_family=AF_INET;
sin.sin_port=htons(WEB_SERVER_PORT);
sin.sin_addr.s_addr=inet_addr(WEB_SERVER_IP);
slen=sizeof(sin);
if(bind(s,(sockaddr *)&sin,slen)==SOCKET_ERROR)
{
strcpy(perror, "Can't bind socket!");
goto error_out;
}
if(listen(s,5)==SOCKET_ERROR)
{
strcpy(perror, "Can't listen on this socket!");
goto error_out;
}
printf("web server running......\n");
slen=sizeof(rsin);
while(true)
{
rs=accept(s,(sockaddr *)&rsin,&slen);
if(rs==INVALID_SOCKET)
{
strcpy(perror, "accept() a INVALID_SOCKET!");
break;
}
bcreate_thread(do_accept, &rs);
}
result=true;
error_out:
if(s!=INVALID_SOCKET) closesocket(s);
if(rs!=INVALID_SOCKET) closesocket(rs);
if(!result) { printf(perror); printf("\n"); }
CPO_Dele(g_cpo);
bsocket_cleanup();
return 0;
}