套接字編寫流程
以 TCP 套接字為例,由於 TCP 是面向連接的協議,所以基於 TCP 的套接字也需要有多個步驟。
套接字的創建
在進行網絡通信之前,都需要使用 socket() 函數創建一個套接字對象。
SOCKET
WSAAPI
socket(
_In_ int af,
_In_ int type,
_In_ int protocol
);
參數 | 說明 |
---|---|
af | socket 使用的地址格式 |
type | 指定套接字的類型 |
protocol | 指定使用的協議類型 |
其中 WinSock 中只支持 AF_INET 作為地址格式,一般 type 確定后 protocol 也會隨之確定。socket 的 type 可以是以下幾種類型:
type | 類型 | 說明 |
---|---|---|
SOCK STREAM | 流套接字 | 使用TCP提供有連接的可靠的傳輸 |
SOCK DGRAM | 數據報套接字 | 使用UDP提供無連接的不可靠的傳輸 |
SOCK RAW | 原始套接字 | 不使用某種特定的協議去封裝它,而是由程序自行處理數據報以及協議首部 |
綁定 socket 和地址
創建了 socket 對象后,需要為該對象綁定 IP 地址和端口號,需要使用到 bind() 函數。
bind(
_In_ SOCKET s,
_In_reads_bytes_(namelen) const struct sockaddr FAR * name,
_In_ int namelen
);
參數 | 說明 |
---|---|
s | 套接字句柄 |
name | 要關聯的本地地址 |
namelen | 地址長度 |
一般來說 s 就是剛剛創建的 socket 對象,name 可一個 sockaddr_in 結構,namelen 則直接對一個 sockaddr_in 結構用 sizeof() 運算即可。
進入監聽狀態
綁定地址后,socket 就可以進入監聽狀態,這個時候就可以接收傳來的鏈接信息了。為了進入監聽狀態,需要使用 listen() 函數。
listen(
_In_ SOCKET s,
_In_ int backlog
);
參數 | 說明 |
---|---|
s | 套接字句柄 |
backlog | 監聽隊列的長度 |
接收連接請求
客戶端想要與服務器建立一條 TCP 連接,需要使用 connect() 函數。
int
WSAAPI
connect(
_In_ SOCKET s,
_In_reads_bytes_(namelen) const struct sockaddr FAR * name,
_In_ int namelen
);
服務器使用 accept() 函數將在監聽隊列中,取出未處理連接中的第一個連接,然后為這個連接創建新的套接字,返回它的句柄。新創建的套接字是處理實際連接的套接字,它與 s 有相同的屬性。
accept(
_In_ SOCKET s,
_Out_writes_bytes_opt_(*addrlen) struct sockaddr FAR * addr,
_Inout_opt_ int FAR * addrlen
);
connect() 函數和 accept() 函數的參數相同:
參數 | 說明 |
---|---|
s | 套接字句柄 |
name | 要連接的設備的地址信息 |
namelen | 地址長度 |
name 中的地址用來尋址遠程的 socket,一般來說監聽狀態下是一個循環等待的過程。此時程序默認工作在阻塞模式下,如果沒有未處理的連接存在,accept() 函數會一直等待下去,直到有新的連接發生才返回。
收發數據
對於流套接字來說,一般使用 send() 函數來發送緩沖區內的數據,返回發送數據的實際字節數。
send(
_In_ SOCKET s,
_In_reads_bytes_(len) const char FAR * buf,
_In_ int len,
_In_ int flags
);
參數 | 說明 |
---|---|
s | 套接字句柄 |
buf | 要發送的數據 |
len | 要發送的數據的長度 |
flags | 調用方式,通常為 0 |
可以使用 recv() 函數從對方接收數據,並將其存儲到指定的緩沖區。
recv(
_In_ SOCKET s,
_Out_writes_bytes_to_(len, return) __out_data_source(NETWORK) char FAR * buf,
_In_ int len,
_In_ int flags
);
參數 | 說明 |
---|---|
s | 套接字句柄 |
buf | 接收的數據要存儲的變量 |
len | 能接收的數據的長度 |
flags | 調用方式,通常為 0 |
在阻塞模式下,send 將會阻塞線程的執行直到所有的數據發送完畢(或者發生錯誤),而 recv 函數將返回盡可能多的當前可用信息,直到達到緩沖區指定的大小。
關閉套接字
當不使用 socket 創建的套接字時,應該調用 closesocket() 函數將它關閉。
int
WSAAPI
closesocket(
_In_ SOCKET s
);
參數 | 說明 |
---|---|
s | 套接字句柄 |
TCP 套接字樣例
功能設計
模擬實現 TCP 協議通信過程,要求編程實現服務器端與客戶端之間雙向數據傳遞。也就是在一條 TCP 連接中,客戶端和服務器相互發送一條數據即可。
程序工作流程
由於前面的流程是對於單個客戶端或服務器的編碼流程,這里給出一組客戶端和服務器工作的流程。
編碼實現
注意無論是客戶端還是服務器,都需要包含頭文件 initsock.h 來載入 Winsock。
initsock.h
#include <winsock2.h>
#pragma comment(lib, "WS2_32") // 鏈接到 WS2_32.lib
class CInitSock
{
public:
/*CInitSock 的構造器*/
CInitSock(BYTE minorVer = 2, BYTE majorVer = 2)
{
// 初始化WS2_32.dll
WSADATA wsaData;
WORD sockVersion = MAKEWORD(minorVer, majorVer);
if (::WSAStartup(sockVersion, &wsaData) != 0)
{
exit(0);
}
}
/*CInitSock 的析構器*/
~CInitSock()
{
::WSACleanup();
}
};
服務器
#include "initsock.h"
#include <iostream>
using namespace std;
CInitSock initSock; // 初始化Winsock庫
int main()
{
// 創建套接字
SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sListen == INVALID_SOCKET)
{
cout << "Failed socket()" << endl;
return 0;
}
// 填充sockaddr_in結構
sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(4567);
sin.sin_addr.S_un.S_addr = INADDR_ANY;
// 綁定這個套接字到一個本地地址
if (::bind(sListen, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)
{
cout << "Failed bind()" << endl;
return 0;
}
// 進入監聽模式
if (::listen(sListen, 2) == SOCKET_ERROR)
{
cout << "Failed listen()" << endl;
return 0;
}
// 循環接受客戶的連接請求
sockaddr_in remoteAddr;
int nAddrLen = sizeof(remoteAddr);
SOCKET sClient;
char szText[] = "你好!";
while (TRUE)
{
cout << "服務端已啟動,正在監聽!\n" << endl;
// 接受一個新連接
sClient = ::accept(sListen, (SOCKADDR*)&remoteAddr, &nAddrLen);
if (sClient == INVALID_SOCKET)
{
cout << "Failed accept()" << endl;
continue;
}
cout << "與主機 " << inet_ntoa(remoteAddr.sin_addr) << "建立連接:" << endl;
// 接收數據
char buff[256];
int nRecv = ::recv(sClient, buff, 256, 0);
if (nRecv > 0)
{
buff[nRecv] = '\0';
cout << "接收到數據:" << buff << endl;
}
// 向客戶端發送數據
::send(sClient, szText, strlen(szText), 0);
// 關閉同客戶端的連接
::closesocket(sClient);
}
// 關閉監聽套接字
::closesocket(sListen);
return 0;
}
客戶端
#include "initsock.h"
#include <iostream>
using namespace std;
CInitSock initSock; // 初始化Winsock庫
int main()
{
// 創建套接字
SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (s == INVALID_SOCKET)
{
cout << " Failed socket()" << endl;
return 0;
}
// 也可以在這里調用bind函數綁定一個本地地址,否則系統將會自動安排
// 填寫遠程地址信息
sockaddr_in servAddr;
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(4567);
// 填寫服務器程序(TCPServer程序)所在機器的IP地址
char serverAddr[] = "127.0.0.1";
servAddr.sin_addr.S_un.S_addr = inet_addr(serverAddr);
//與服務器建立連接
if (::connect(s, (sockaddr*)&servAddr, sizeof(servAddr)) == -1)
{
cout << " Failed connect()" << endl;
return 0;
}
cout << "與服務器 " << serverAddr << "建立連接" << endl;
//向服務器發送數據
char szText[] = "你好,服務器!";
int slen = send(s, szText, 100, 0);
if (slen > 0)
{
cout << "向服務器發送數據:" << szText << endl;
}
// 接收數據
char buff[256];
int nRecv = ::recv(s, buff, 256, 0);
if (nRecv > 0)
{
buff[nRecv] = '\0';
cout << "接收到數據:" << buff << endl;
}
// 關閉套接字
::closesocket(s);
return 0;
}
運行效果
參考資料
《Windows 網絡與通信編程》,陳香凝 王燁陽 陳婷婷 張錚 編著,人民郵電出版社