1 前言
2 Socket編程
2.1 Socket通信機(jī)制
2.2 socket通信示例圖
2.3 Socket在不同平臺(tái)上的實(shí)現(xiàn)
2.3.1 Socket在Windows平臺(tái)中的實(shí)現(xiàn)
2.3.2 Socket在Linux/Unix平臺(tái)中的實(shí)現(xiàn)
2.3.3 可移植的啟動(dòng)和結(jié)束調(diào)用代碼
2.3.4 其它移植問題
3 多線程編程
3.1 線程與進(jìn)程的不同
3.2 線程沖突與數(shù)據(jù)保護(hù)
3.3 Win32中的線程
3.3.1 線程同步
3.3.2 創(chuàng)建線程
3.4 Linux/Unix中的線程
3.5 可移植的線程代碼
4 程序?qū)嵗?
前言
Socket編程特別是多線程編程是一個(gè)很大的課題,本文針對(duì)公司最近將要實(shí)現(xiàn)的下載版和網(wǎng)頁版的CPR和CPE兩個(gè)軟件來講解socket和多線程的可移植編程技術(shù),所涉及的是其中較基礎(chǔ)的部分,以盡量滿足當(dāng)前公司需要為準(zhǔn)。
Socket編程
Socket通信機(jī)制通過socket可實(shí)現(xiàn)點(diǎn)對(duì)點(diǎn)或廣播通信程序,兩者之間的區(qū)別不大,編程時(shí)其程序流程所用代碼幾乎相同,不同的地方在于目標(biāo)地址選擇的不同。本教材所舉實(shí)例為點(diǎn)對(duì)點(diǎn)的形式,即以客戶/服務(wù)器形式來實(shí)現(xiàn)一個(gè)socket通信程序,為描述方便,我們對(duì)兩點(diǎn)分別命名為Server和Client。Socket實(shí)現(xiàn)Server 與Client之間的通信的過程,非常象電信局的普通電話服務(wù),為了更好的理解socket,以下就以電信局的電話服務(wù)作為類比來說明socket的通信機(jī)制:
* 電信局提供電話服務(wù)類似我們這的Server,普通電話用戶類似我們這的Client。
* 首先電信局必須建立一個(gè)電話總機(jī)。這就如果我們必須在Server端建立一個(gè)Socket(套接字),這一步通過調(diào)用socket()函數(shù)實(shí)現(xiàn)。
* 電信局必須給電話總機(jī)分配一個(gè)號(hào)碼,以便使用戶要撥找該號(hào)碼得到電話服務(wù),同時(shí)接入該電信局的用戶必須知道該總機(jī)的號(hào)碼。同樣,我也在Server端也要為這一套接字指定一port(端口),并且要連接該Server的Client必須知道該端口。這一步通過調(diào)用bind()函數(shù)實(shí)現(xiàn)。
* 接下來電信局必須使總機(jī)開通并使總機(jī)能夠高效地監(jiān)聽用戶撥號(hào),如果電信局所提供服務(wù)的用戶數(shù)太多,你會(huì)發(fā)現(xiàn)撥打電信局總機(jī)老是忙音,通常電信局內(nèi)部會(huì)使該總機(jī)對(duì)應(yīng)的電話號(hào)碼連到好幾個(gè)負(fù)責(zé)交換的處理中心,在一個(gè)處理中心忙于處理當(dāng)前的某個(gè)用戶時(shí),新到用戶可自動(dòng)轉(zhuǎn)到一下處理中心得到服務(wù)。同樣我們的Server端也要使自己的套接口設(shè)置成監(jiān)聽狀態(tài),這是通用listen()函數(shù)實(shí)現(xiàn)的,listen()的第二個(gè)參數(shù)是等待隊(duì)列數(shù),就如同你可以指定電信局的建立幾個(gè)負(fù)責(zé)交換的處理中心。
* 用戶知道了電信局的總機(jī)號(hào)后就可以進(jìn)行撥打請(qǐng)求得到服務(wù)。在Winsock的世界里做為Client端是要先用socket()函數(shù)建立一個(gè)套接字,然后調(diào)connect()函數(shù)進(jìn)行連接。當(dāng)然和電話一樣,如果等待隊(duì)列數(shù)滿了、與Server的線路不通或是Server沒有提供此項(xiàng)服務(wù)時(shí),連接就不會(huì)成功。
* 電信局的總機(jī)接受了這用戶撥打的電話后負(fù)責(zé)接通用戶的線路,而總機(jī)本身則再回到等待的狀態(tài)。Server也是一樣,調(diào)用accept()函數(shù)進(jìn)入監(jiān)聽處理過程,Server端的代碼即在中處暫停,一旦Server端接到申請(qǐng)后系統(tǒng)會(huì)建立一個(gè)新的套接字來對(duì)此連接做服務(wù),而原先的套接字則再回到監(jiān)聽等待的狀態(tài)。
* 當(dāng)你電話掛完了,你就可以掛上電話,彼此間也就離線了。Client和Server間的套接字的關(guān)閉也是如此;這個(gè)關(guān)閉離線的動(dòng)作,可由Client端或Server端任一方先關(guān)閉,這也與電話局的服務(wù)類似。
從以上情況可以看出在服務(wù)器端建立一個(gè)套接字,進(jìn)入監(jiān)聽狀態(tài)到接收到一個(gè)客戶端的請(qǐng)求的過程如下:
socket()->bind()->listen()->accept()->recv()->send()
在客戶端則要簡(jiǎn)單得多,其調(diào)用過程如下:
socket()->connect()->send()->recv()
socket通信示例圖
下圖顯示的通信方式用在許多場(chǎng)合,比如HTTP協(xié)議中用的就是這種方式。FTP、TELNET等協(xié)議也與此大致相同,不同之外在于FTP、TELNET這類協(xié)議會(huì)多次重復(fù)send()和recv()的調(diào)用。
Socket在不同平臺(tái)上的實(shí)現(xiàn)
Socket在Windows平臺(tái)中的實(shí)現(xiàn)
Socket在Windows中的實(shí)現(xiàn)稱為Winsock,核心文件是winsock.dll(或winsock32.dll)。這個(gè)動(dòng)態(tài)鏈接庫負(fù)責(zé)提供標(biāo)準(zhǔn)的socket調(diào)用API和其它幾個(gè)特定的Windows平臺(tái)專用API,另外它還實(shí)現(xiàn)了適用于Windows消息機(jī)制的同步socket通信機(jī)制。在Winsock中所有非標(biāo)準(zhǔn)的socket調(diào)用均以WSA三個(gè)字母開頭命名。
由于Win32的各個(gè)平臺(tái)均支持線程,我采用同步socket通信機(jī)制來編寫Winsock程序不可取。一是多線程的系統(tǒng)中對(duì)同步通信有更好的解決辦法,二是同步通信機(jī)制程序沒有很好的兼容性。
Winsock中有兩個(gè)比較重要的函數(shù)就是WSAStartup()和WSACleanup(),它們的作用在于對(duì)Winsock動(dòng)態(tài)鏈接庫進(jìn)行一些初始化或清除工作。在一個(gè)進(jìn)程中,沒有調(diào)用WSAStartup()之前將無法正確調(diào)用任何其它標(biāo)準(zhǔn)的socket函數(shù)。
Socket在Linux/Unix平臺(tái)中的實(shí)現(xiàn)
Socket最早是在Unix平臺(tái)上實(shí)現(xiàn)的,socket調(diào)用已是當(dāng)今Unix平臺(tái)的標(biāo)準(zhǔn)系統(tǒng)調(diào)用。Linux的網(wǎng)絡(luò)部分模塊幾乎是原封不動(dòng)的從Unix中遷移過來的,其函數(shù)的調(diào)用方式基本與Unix相同。
在Linux/Unix平臺(tái)中,如果你正在向一個(gè)套接字發(fā)信息,而遠(yuǎn)端計(jì)算機(jī)又關(guān)閉了該端口,將產(chǎn)生一個(gè)SIGPIPE信號(hào),該信號(hào)的默認(rèn)處理過程將中止當(dāng)前進(jìn)程,為使我們的socket程序能夠健壯的運(yùn)行,必須接管該信息的處理過程,一般情況使系統(tǒng)忽略該信號(hào)即可。
可移植的啟動(dòng)和結(jié)束調(diào)用代碼
根據(jù)以上兩點(diǎn)所述的socket中不同平臺(tái)中的實(shí)現(xiàn)方式,可以編寫出可移植的socket初始化和結(jié)束調(diào)用代碼如下:
bool bsocket_init() //初始化socket 調(diào)用
{
#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信號(hào)
return true;
#endif
}
void bsocket_cleanup() //結(jié)束socket 調(diào)用
{
#ifdef _WIN32
WSACleanup();
#else
signal(SIGPIPE, SIG_DFL); //恢復(fù)SIGPIPE信號(hào)
return;
#endif
}
其它移植問題
關(guān)閉socket
Windows中關(guān)閉socket的調(diào)用為closesocket(socket sock_in); Linux為close(socket sock_in);
Socket地址長(zhǎng)度類型
Socket的許多調(diào)用中均用到socket地址長(zhǎng)度做為參數(shù),該參數(shù)在Windows中為int類型,在Linux中為socklen_t類型。
無效的socket
Windows中調(diào)用socket()和accept()時(shí),如果返回的是無效的socket,則該值為INVALID_SOCKET,Linux中為-1。
調(diào)用錯(cuò)誤返回
Windows中除上述的socket()和accept()以及幾個(gè)少數(shù)的調(diào)用外,大部分調(diào)用在出錯(cuò)時(shí)返回值為SOCKET_ERROR,Linux中為-1。
多線程編程
線程與進(jìn)程的不同
當(dāng)把一個(gè)可執(zhí)行程序裝載在內(nèi)存中后,不管你是你是準(zhǔn)備它或是已經(jīng)執(zhí)行它或是被暫停執(zhí)行,它都被稱為一個(gè)進(jìn)程。你可以稱一個(gè)進(jìn)程為一個(gè)可執(zhí)行程序在內(nèi)存中的實(shí)例。雖然一個(gè)進(jìn)程可以復(fù)制自己,也可以調(diào)用其它進(jìn)程或與其它進(jìn)程通信,全基本上進(jìn)程是由操作系統(tǒng)直接管理的資源。每一個(gè)進(jìn)程均有一個(gè)與其它進(jìn)程獨(dú)立的操作系統(tǒng)運(yùn)行環(huán)境,比如環(huán)境變量、當(dāng)前目錄等。
線程是一種可執(zhí)行體,它表示對(duì)CPU運(yùn)行時(shí)間的占有權(quán),線程也是操作系統(tǒng)中的一種資源,它的創(chuàng)建、中止最終要由操作系統(tǒng)來實(shí)現(xiàn)。但線程每一個(gè)線程更多表現(xiàn)為由一個(gè)進(jìn)程直接來管理。每一個(gè)線程均有一個(gè)與其它線程不同的硬件運(yùn)行環(huán)境,比如各CPU中通用寄存器和狀態(tài)寄存器的值。
一個(gè)進(jìn)程的創(chuàng)建需要操作系統(tǒng)做大量工作,如設(shè)置環(huán)境變量、裝裁可執(zhí)行代碼、分配進(jìn)程資源等,進(jìn)程間的切換也需做大量工作。而線程的創(chuàng)建的切換相對(duì)來說要簡(jiǎn)單得多,操作也能夠以最快的速度來實(shí)現(xiàn)不同執(zhí)行體之間的切換。
另一方面,由于所有線程共享一個(gè)進(jìn)程空間,線程間的通信變得十分簡(jiǎn)單;而進(jìn)程之間由于各自內(nèi)存相互隔離,進(jìn)程之間通信只能通過管道、發(fā)送系統(tǒng)消息或信號(hào)來實(shí)現(xiàn)。
線程沖突與數(shù)據(jù)保護(hù)
由于同一進(jìn)程中的線程由操作系統(tǒng)并發(fā)執(zhí)行。當(dāng)一個(gè)進(jìn)程中的許多線程要修改某些公用變量時(shí)就存在數(shù)據(jù)保護(hù)問題。解決這個(gè)問題的通常辦法是向操作系統(tǒng)申請(qǐng)一個(gè)線程同步對(duì)象,操作系統(tǒng)只允許同時(shí)有一個(gè)少數(shù)個(gè)線程訪問受保護(hù)的數(shù)據(jù)。
Win32中的線程
線程同步
Win32中可用于線程同步的對(duì)象有CRITICAL_SECTION, Event, Mutex, Semaphore和Waitable timer等。 其中CRITICAL_SECTION比較適于解決在單一進(jìn)程多線程的沖突問題。
與CRITICAL_SECTION有關(guān)的幾個(gè)函數(shù)為:
* InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
該函數(shù)初始化一個(gè)CRITICAL_SECTION。
* DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
該函數(shù)清除一個(gè)CRITICAL_SECTION。
* EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
該函數(shù)使本線程取得CRITICAL_SECTION控制權(quán),代碼進(jìn)入保護(hù)狀態(tài)。
* LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
該函數(shù)使本線程放棄CRITICAL_SECTION控制權(quán),代碼退出保護(hù)狀態(tài)。
創(chuàng)建線程
Win32中最常用的創(chuàng)建線程的函數(shù)為CreateThread(),該函數(shù)的格式為:
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
DWORD dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId);
* lpThreadAttributes: 輸入?yún)?shù)。指定是否本次函數(shù)返回的線程句柄可以被子進(jìn)程繼承,可以為NULL。在CPR/CPE兩個(gè)產(chǎn)品下載版和網(wǎng)頁版的開發(fā)過程代碼使用NULL應(yīng)可滿足需要。
* dwStackSize : 輸入?yún)?shù)。指定指定的線程堆棧大小,可以為0,如果為0,使用默認(rèn)堆棧大小。在CPR/CPE兩個(gè)產(chǎn)品下載版和網(wǎng)頁版的開發(fā)過程代碼使用0應(yīng)可滿足需要。
* lpStartAddress : 輸入?yún)?shù)。指向線程函數(shù),該函數(shù)包含線程的執(zhí)行代碼。
* lpParameter : 輸入?yún)?shù)。指定要傳給線程的參數(shù)塊內(nèi)存地址。
* lpThreadId : 輸出參數(shù)。該參數(shù)在調(diào)用結(jié)束后將包含被創(chuàng)建線程的ID。
Linux/Unix中的線程
當(dāng)前各版本的Linux和Unix本身并不支持線程,但在Linux/Unix各版本均自帶pthread線程包及它們的開發(fā)運(yùn)行頭文件和庫文件。
與Win32類似,pthread包中用于線程同步的對(duì)象也有多種,在我們的開發(fā)應(yīng)用中,使用pthread_mutex_t即可滿足需求。與pthread_mutex_t相關(guān)的幾個(gè)函數(shù)為:
*
pthread_mutex_init(pthread_mutex_t * p, pthread_mutexattr_t * pa);
該函數(shù)初始化一個(gè)pthread_mutex_t,其中pa參數(shù)可以設(shè)置pthread_mutex_t的屬性。與Win32中多線程的機(jī)制稍有不同,在默認(rèn)情況下,pthread中對(duì)同一線程的多次保護(hù)請(qǐng)求會(huì)造成互鎖,以我們產(chǎn)品開發(fā)的情況來看,要修改pthread的屬性方可滿足實(shí)際需要,但pthread中設(shè)置多線程同步屬性的方法的可移植性不高。對(duì)同一線程多次申請(qǐng)保護(hù)的問題可以記錄線程ID的辦法來解決。
* pthread_mutex_destroy(pthread_mutex_t * p);
清除一個(gè)已初始化的pthread_mutex_t。
* pthread_mutex_lock(pthread_mutex_t * p);
請(qǐng)求取得pthread_mutex_t控制權(quán),進(jìn)入代碼保護(hù)。
* pthread_mutex_unlock(pthread_mutex_t * p);
放棄一個(gè)pthread_mutex_t控制權(quán),退出代碼保護(hù)。
可移植的線程代碼
(參見程序?qū)嵗?
程序?qū)嵗?
#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;
}