一, 什麼是Socket
接觸網絡程式設計當然要了解Socket,Socket(套接字)是一種網絡程式設計接口,Socket提供了很多靈活的函數,幫助程式員寫出高效的網絡應用。Socket分為BSD UNIX和windows兩個版本。
在win32平台上的Winsock程式設計都要經過下列基本步驟:
定義變量——獲得Winsock版本——加載Winsock庫——初始化——建立套接字——設定套接字——關閉套接字——解除安裝Winsock庫
使用winsock2 API程式設計,必須包含頭檔案winsock2.h (連結環境WS2_32.LIB),頭檔案winsock.h(WSOCK32.LIB)是為了相容winsock1程式時使用的,另外mswsock.h(MSWSOCK.DLL)是微軟的擴充類,用于開發高性能的winsock程式。
準備好後,你就可以着手建立你的第一個網絡程式了。
二, Socket程式設計的基本過程
Socket通信分為面向連接配接的通信(TCP)和面向無連接配接的通信(UDP),通信流程如下:
面向連接配接的通信
無連接配接的通信
1,Winsock初始化和結束
每一個winsock應用程式必須首先加載相應的winsock dll版本。方法是調用:
int WSAStartup(
WORD wVersionRequested, 庫版本,高位元組副版本,低位元組主版本
LPWSADATA lpWSAData 結構指針,函數自動填充該結構。
); 函數調用成功傳回0
可以用宏MAKEWORD(x, y)用來指定第一個參數的值
2,建立套接字
套接字是傳輸提供者的一個句柄。
SOCKET socket (
int af,
int type,
int protocol IPPROTO_TCP, IPPROTO_UDP, 0(如果不想指定)
);
第一個參數指定通信協定的協定族,AF_INET(IPv4)或 AF_INET6(IPv6)(因為Socket是網絡程式設計接口而不是一個協定,它使用流行的網絡協定(TCP/IP,IPX)為應用程式提供的一個程式設計接口。)
第二個參數指定要建立的套接字的類型。SOCK_STREAM(TCP流套接字), SOCK_ DGRAM(UDP 資料包套接字),SOCK_RAW(原始套接字)
第三個參數指定應用程式所指定應用程式所使用的通信協定。
函數成功傳回套接字描述符,失敗傳回INVALID_SOCKET
3,配置套接字
當建立一個套接字後,再進行網絡通信之前,必須先配置Socket。面向連接配接的用戶端Socket通過調用connect函數在Socket資料結構中儲存位址和遠端資訊。無連接配接用戶端,服務端以及面向連接配接Socket的服務端,通過調用bind函數來配置本地資訊。
int bind(
SOCKET s, 建立的套接字
const struct sockaddr FAR* name, 指向位址緩沖區的指針
int namelen 位址緩沖區的大小
);
成功傳回0,失敗傳回SOCKET_ERROR
當建立一個套接字後,套接字資料結構中有一個預設的IP位址和預設的端口号。一個服務程式必須調用bind函數來給其綁定一個IP位址和一個特定的端口号。
第二個參數指定一個sockaddr結構定義如下:
struct sockaddr {
u_short sa_family;
char sa_data[14];
};
我們通常使用另外一個等價的位址結構:
struct sockaddr_in {
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
其中sin_family是通信協定族,
sin_port 指明端口号,
sin_addr 結構中有一個字段s_addr,表示IP位址,該字段是一個整數,
一般用函數inet_addr把點分字元串形式的IP位址轉化成unsigned long型的整數值。
如果指定為htonl(INADDR_ANY),那麼無論哪個網段上的客戶機都能與該伺服器通信,
否則,隻有與指定IP位址處于同一網段上的客戶機能與該伺服器通信。
sin_zero[8] 為填充,使兩個結構大小相同。
有一些細節學要說明:
在計算機把IP位址和端口号指定成多位元組時,這個數是按“主機位元組”(host-byte)順序表示的,不同的處理器對數的表示方法有“大頭”(big-endian——最有意義的位元組到最無意義的位元組)和“小頭”(little-endian)兩種形式。但是如果在網絡上指定IP位址和端口号時,必須按照big-endian 的形式,一般稱之為“網絡位元組”(network-byte)順序。
以下幾個函數将主機位元組順序轉換成網絡位元組順序:
u_long htonl(u_long hostlong);
int WSAHtonl(
SOCKET s,
u_long hostlong,
u_long FAR * lpnetlong 通過指針傳回網絡位元組順序的4個位元組的數
);
u_short htons(u_short hostshort);
int WSAHtons(
SOCKET s,
u_short hostshort,
u_short FAR * lpnetshort 通過指針傳回網絡位元組順序的2個位元組的數
);
以下幾個函數将網絡位元組順序轉換成主機位元組順序:
u_long ntohl(u_long netlong);
int WSANtohl(
SOCKET s,
u_long netlong,
u_long FAR * lphostlong
);
u_short ntohs(u_short netshort);
int WSANtohs(
SOCKET s,
u_short netshort,
u_short FAR * lphostshort
);
4,實作功能
(1) 伺服器端: 需要對綁定的端口進行偵聽,函數原型如下
int listen (
SOCKET s ,
int backlog
);
Backlog 是客戶連接配接請求隊列的最大數量,而不是客戶機連接配接的數量限制。 處于偵聽的套接字将維護一個客戶連接配接請求隊列。 該函數執行成功傳回0,失敗傳回 SOCKET_ERROR
此外,需要從連接配接請求隊列中取出最前面的一個客戶請求,需要用到accept()函數:
SOCKET accept (
SOCKET s ,
struct sockaddr FAR* addr ,
int FAR* addrlen
);
該函數建立一個新的套接字來與客戶套接字建立通信,如果連接配接成功,就傳回建立的套接字描述符,
以後與客戶通信的就是該套接字,而偵聽套接字則繼續接受新的連接配接;如果失敗就傳回 INVALID_SOCKET
第一個參數是偵聽套接字
第二個套接字用來傳回新建立的套接字的位址結構
第三個套接字傳回位址結構的長度
(2) 用戶端:
connect函數是客戶機建立與遠端伺服器連接配接而使用的。
int connect (
SOCKET s ,
const struct sockaddr FAR* name ,
int namelen
);
5,資料傳輸
收發資料時網絡程式設計的一切在這裡我們隻讨論同步函數send和recv,
不讨論異步函數WSASend和WSARecv。
資料發送要用到send函數,原型如下:
int send(
SOCKET s,
const char FAR * buf, 發送資料緩沖區的位址
int len, 要發送的位元組數
int flags 一般為0, MSG_DONTROUTE, MSG_OOB(外帶資料)
);
成功傳回發送位元組數,出錯傳回SOCKET_ERROR
注意send函數把資料從buf複制到socket發送緩沖區後就傳回了,
但是這些資料并不時馬上就發送到連接配接的另一端。
在已連接配接的套接字上接受資料,recv函數是最基本的方式。
int recv(
SOCKET s,
char FAR* buf,
int len, 準備接收的位元組數或buf緩沖區長度
int flags 0, MSG_PEEK, MSG_OOB 其中MSG_PEEK表示将有用的資料複制到所提供的接收端緩沖區,
但是沒有從系統緩沖區中将它删除
);
成功傳回接收的資料的位元組數量,失敗傳回SOCKET_ERROR,如果接受資料時網絡中斷傳回0
6,關閉Socket
一旦完成任務,記得将套接字關閉以釋放資源:
int closeSocket(SOCKET s)
三, Socket程式設計執行個體
講了這麼多其實還是看執行個體最為重要了,下面我提供了最簡單的面向連接配接的用戶端和服務端程式,
當服務端接受用戶端的連接配接後,先是該客戶機地IP位址,并向用戶端發送一個回應消息,
最後關閉該連接配接套接字。這樣的伺服器當然沒什麼實際的用途。
設計一個基本的網絡伺服器有以下幾個步驟:
1、初始化Windows Socket
2、建立一個監聽的Socket
3、設定伺服器位址資訊,并将監聽端口綁定到這個位址上
4、開始監聽
5、接受用戶端連接配接
6、和用戶端通信
7、結束服務并清理Windows Socket和相關資料,或者傳回第4步
#include <winsock2.h>
#include <stdio.h>
#define SERVPORT 5050
#pragma comment(lib,"ws2_32.lib")
void main(void)
{
WSADATA wsaData;
SOCKET sListen; // 監聽socket
SOCKET sClient; // 連接配接socket
SOCKADDR_IN serverAddr; // 本機位址資訊
SOCKADDR_IN clientAddr; // 用戶端位址資訊
int clientAddrLen; // 位址結構的長度
int nResult;
// 初始化Windows Socket 2.2
WSAStartup(MAKEWORD(2,2), &wsaData);
// 建立一個新的Socket來響應用戶端的連接配接請求
sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// 填寫伺服器綁定的位址資訊
// 端口為5150
// IP位址為INADDR_ANY,響應每個網絡接口的客戶機活動
// 注意使用htonl将IP位址轉換為網絡格式
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(SERVPORT);
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
memset(&(serverAddr.sin_zero), 0, sizeof(serverAddr.sin_zero));
// 綁定監聽端口
nResult = bind(sListen, (SOCKADDR *)&serverAddr, sizeof(SOCKADDR));
if (nResult == SOCKET_ERROR)
{
printf("bind failed!/n");
return;
}
// 開始監聽,指定最大接受隊列長度5,不是連接配接數的上限
listen(sListen, 5);
// 接受新的連接配接
while(1)
{
clientAddrLen = sizeof (SOCKADDR);
sClient = accept(sListen, (SOCKADDR *)&clientAddr, &clientAddrLen);
if(sClient == INVALID_SOCKET)
{
printf("Accept failed!");
}
else
{
printf("Accepted client: %s : %d/n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));
// 向用戶端發送資訊
nResult = send(sClient, "Connect success!", 16, 0);
if (nResult == SOCKET_ERROR)
{
printf("send failed!");
}
}
// 我們直接關閉連接配接,
closesocket(sClient);
}
// 并關閉監聽Socket,然後退出應用程式
closesocket(sListen);
// 釋放Windows Socket DLL的相關資源
WSACleanup();
}
客戶機程式如下:
#include <winsock2.h>
#include <stdio.h>
#define SERVPORT 5050 // 端口為5150
#define MAXDATASIZE 100
#define SERVIP "127.0.0.1" // 伺服器IP位址為"127.0.0.1",注意使用inet_addr将IP位址轉換為網絡格式
#pragma comment(lib,"ws2_32.lib")
void main(int argc, char *argv[])
{
WSADATA wsaData;
SOCKET sConnect;
SOCKADDR_IN serverAddr;
int recvbytes;
char buf[MAXDATASIZE];
//初始化Windows Socket 2.2
WSAStartup(MAKEWORD(2,2), &wsaData);
// 建立一個新的Socket來連接配接伺服器
sConnect = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// 填寫連接配接位址資訊
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(SERVPORT);
serverAddr.sin_addr.s_addr = inet_addr(SERVIP);
memset(&(serverAddr.sin_zero), 0, sizeof(serverAddr.sin_zero));
// 向伺服器發出連接配接請求
if (connect(sConnect, (SOCKADDR *)&serverAddr, sizeof(SOCKADDR)) == SOCKET_ERROR)
{
printf("connect failed!/n");
return;
}
// 接受伺服器的回應消息
recvbytes = recv(sConnect, buf, MAXDATASIZE, 0);
if (recvbytes == SOCKET_ERROR)
{
printf("recv failed!/n");
}
else
{
buf[recvbytes] = '/0';
printf("%s/n",buf);
}
closesocket(sConnect);
// 釋放Windows Socket DLL的相關資源
WSACleanup();
}
四, 結束語
這裡介紹的隻不過是winsock最基礎最基礎的知識,開發網絡程式從來都不是一件容易的事情,
盡管隻需要遵守很少的一些規則: 建立套接字,發起連接配接,接受連接配接,
發送和接受資料。真正的困難在于:讓你的程式可以适應從單單一個連接配接到幾千個連接配接,
即開發一個大容量,具可擴充性的Winsock應用程式。
Winscok程式設計很重要的一部分是IO處理,分别提供了“套接字模式”和“套接字I/O模型”,
可對一個套接字上的I/O行為進行控制。
其中,套接字模式用于決定在随一個套接字調用時,那些Winsock函數的行為,
有阻塞和非阻塞兩種模式。
而另一方面,套接字模型描述了一個應用程式如何對套接字上進行的I/O進行管理及處理,
包括包括Select,WSAAsyncSelect,WSAEventSelect ,IO重疊模型,完成端口等。
在這裡推薦一本好書《windows網絡程式設計技術》,有興趣研究的同志可以去啃一啃。