注:本系列文章適合初學網絡編程的讀者
網絡程序的實現可以有很多方式,Windows Socket就是其中一種比較簡單的方法。socket是連接應用程序與網絡驅動程序的橋梁,socket在應用程序中創建,通過綁定操作與驅動程序建立關系。此后,應用程序送給socket的數據,由socket交給驅動程序向網絡上發送出去。計算機從網絡上收到與該socket綁定的IP地址和端口號相關的數據后,由驅動程序交給socket,應用程序便可從該socket中提取接收到的數據。
在TCP/IP網絡應用中,通信的兩個進程間相互作用的主要是(client/server)模式,即客戶向服務器提出請求,服務器接收到請求后,提供相應的服務。
下面通過一個簡單的實例來講述基于TCP的socket編程的通信流程。其中服務器端程序實現代碼TCPSrv.cpp如下:

Server
1
//#include <windows.h>
2
#include <Winsock2.h>
3
#include <stdio.h>
4
5
void main()
6

{
7
//加載套接字庫
8
WORD wVersionRequested;
9
WSADATA wsaData;
10
int err;
11
12
wVersionRequested = MAKEWORD(1, 1);
13
err = WSAStartup(wVersionRequested, &wsaData);
14
if( err != 0 )
15
{
16
return;
17
}
18
if( LOBYTE(wsaData.wVersion) != 1 ||
19
HIBYTE(wsaData.wVersion) != 1)
20
{
21
WSACleanup();
22
return;
23
}
24
25
//創建用于監聽的套接字
26
SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);
27
SOCKADDR_IN addSrv;
28
addSrv.sin_addr.S_un.S_addr = htonl( INADDR_ANY );
29
addSrv.sin_family = AF_INET;
30
addSrv.sin_port = htons(6000);
31
32
//綁定套接字
33
bind(sockSrv, (SOCKADDR*)&addSrv, sizeof(SOCKADDR));
34
//將套接字設為監聽模式,準備接收客戶請求
35
listen(sockSrv, 5);
36
37
SOCKADDR_IN addClient;
38
int len = sizeof(SOCKADDR);
39
40
while(1)
41
{
42
//等待客戶請求
43
SOCKET sockConn = accept(sockSrv, (SOCKADDR*)&addClient, &len);
44
char sendBuf[1000];
45
sprintf(sendBuf, "Welcome %s to http://www.cnblogs.com/lantionzy", inet_ntoa(addClient.sin_addr));
46
//發送數據
47
send(sockConn, sendBuf, strlen(sendBuf)+1, 0);
48
49
char receiveBuf[1000];
50
//接收數據
51
recv(sockConn, receiveBuf, 1000, 0);
52
//打印接收的數據
53
printf("%s\n", receiveBuf);
54
//關閉套接字
55
closesocket(sockConn);
56
}
57
}
在這段代碼中,首先定義了一個WORD類型的變量:wVersionRequested,用來保存WinSocl庫的版本號,接著調用MAKEWORD宏創建一個包含了請求版本號的WORD值,之后調用WSAStartup函數加載套接字庫,如果其返回值不等于0 ,則程序退出。接下來判斷wsaData.wVersion的低字節和高字節是否都等于1,如果不是我們請求的版本,那么調用WSACleanup函數終止對Winsock庫的使用并返回。
加載套接字庫后,就可以按照一定流程來編寫實現代碼了:
1、創建套接字(socket)
利用socket函數創建套接字,對于它來說,第一個參數只能是AF_INET或(PF_INET);本例是基于TCP協議的網絡程序,需要創建的是流式套接字,因此將socket函數第二個參數設置為SOCK_STREAM;將其第三個參數指定為0。這樣該函數將根據地址格式和套接字類別,自動選擇一個合適的協議。
2、將套接字綁定到一個本地地址和端口上(bind)
在SOCKADDR_IN結構體中,除了sa_family成員外,其他成員都是按網絡字節順序表示的。因此使用htonl函數將INADDR_ANY值轉換為網絡字節順序。調用bind函把套接字sockSrv綁定到本地地址和指定端口上。其第一個參數為要綁定的套接字,第二個需要一個指針,可以用取地址符來實現,并且addrSrv變量是SOCKADDR_IN結構體類型,而這里需要的是SOCKADDR*類型,所以要進行強制轉換。第三個參數是指定地址結構的大小,可以利用sizeof操作符來獲取。
3、將套接字設為監聽模式(listen),準備接收客戶請求。其中listen函數第二個參數是指等待連接隊列的最大長度。
4、等待客戶請求的到來;當請求到來后,接受連接請求,返回一個新的對應于此連接的套接字(accept)。
接下來,需要調用accept函數等待并接受客戶的連接請求。因為作為服務器端,它需要不斷的等待客戶端的連接請求的到來,所以設計成一個死循環。當客戶端有請求時,該函數接受請求建立連接,同時返回一個相對于當前這個新連接的一個套接字描述符,保存于sockConn變量中,然后利用這個套接字就可以與客戶端進行通信了,而我們先前的套接字仍繼續監聽客戶端的連接請求。
5、用返回的套接字和客戶端進行通信(send/recv)
可以調用send函數向客戶端發送數據,注意這里使用的套接字是已建立連接的那個套接字:sockConn,而不是用于監聽的那個套接字:addrSrv。使用recv函數從客戶端接收數據。
6、返回等待另一個客戶請求
7、關閉套接字
上面實現的服務器端的程序,下面是客戶端程序實現代碼TCPClient.cpp:

Client
1
//#include <windows.h>
2
#include <Winsock2.h>
3
#include <stdio.h>
4
5
int main()
6

{
7
WORD wVersionRequested;
8
WSADATA wsaData;
9
int err;
10
11
wVersionRequested = MAKEWORD(1, 1);
12
err = WSAStartup(wVersionRequested, &wsaData);
13
if( err != 0 )
14
{
15
return -1;
16
}
17
if( LOBYTE(wsaData.wVersion) != 1 ||
18
HIBYTE(wsaData.wVersion) != 1)
19
{
20
WSACleanup();
21
return -1;
22
}
23
24
SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);
25
SOCKADDR_IN addSrv;
26
addSrv.sin_addr.S_un.S_addr = inet_addr( "127.0.0.1" );
27
addSrv.sin_family = AF_INET;
28
addSrv.sin_port = htons(6000);
29
30
connect(sockClient, (SOCKADDR*)&addSrv, sizeof(SOCKADDR));
31
32
char receiveBuf[1000];
33
recv(sockClient, receiveBuf, 1000, 0);
34
printf("%s\n", receiveBuf);
35
36
send(sockClient, "This is lantionzy", strlen("This is lantionzy")+1, 0);
37
closesocket(sockClient);
38
39
WSACleanup();
40
}
對于客戶端來說,它不需要邦定,可以直接連接服務器端。
首先運行服務器端程序,然后再運行客戶端程序,可以看到客戶端收到了服務器端返回的信息:Welcome 127.0.0.1 to http://www.shnenglu.com/lantionzy,而服務器端收到了客戶端發送的信息:This is lantionzy。
剖析網絡編程(2)-- 基于UDP的的網絡應用程序 剖析網絡編程(3)-- 基于TCP/UDP網絡編程應注意的幾個地方