參考
- 《TCP/IP網絡程式設計》 尹聖雨
面向連接配接的伺服器端和用戶端的編寫
了解TCP和UDP
根據資料傳輸方式的不同,基于網絡協定的套接字一般分為TCP套接字和UDP套接字。因為TCP套接字是面向連接配接的,是以又稱為流(stream)的套接字
TCP是Transmission Control Protocol(傳輸控制協定)的簡寫,意為“對資料傳輸過程的控制”
TCP/IP的協定棧
TCP/IP協定棧共分為4層,可以了解資料收發分成了4個階層化過程。(1)應用層;(2)TCP/UDP層;(3)IP層;(4)鍊路層。各層可能通過作業系統等軟體實作,也可能同構類似NIC的硬體裝置實作
把協定分成多個層次的優點:(1)協定設計更容易;(2)為了通過标準化操作設計開放式系統
鍊路層
鍊路層是實體連結領域标準化的結果,也是最基本的領域,專門定義LAN、WAN、MAN等網絡标準
IP層
為了在複雜的網絡中傳輸資料,首先需要考慮路徑的選擇。IP層解決向目标傳輸資料需要經過哪條路徑,該層使用的協定就是IP
IP本身是面向消息的、不可靠的協定。每次傳輸資料時會幫我們選擇路徑,但并不一緻。如果傳輸中發生路徑錯誤,則選擇其他路徑;但如果發生資料丢失或錯誤,則無法解決。即,IP協定無法應對資料錯誤
TCP/UDP層
TCP和UDP層以IP層提供的路徑資訊為基礎完成實際的資料傳輸,故該層又稱為傳輸層(Transport)
IP層隻關注1個資料包的傳輸過程。是以,即使傳輸多個資料包,每個資料包也是由IP層實際傳輸的,即,傳輸順序及傳輸本身是不可靠的。若隻利用IP層傳輸資料,則有可能導緻後傳輸的資料包B比先傳輸的資料包A提早到達,也有可能某個資料包損毀。
綜上,TCP和UDP存在于IP層之上,決定主機之間的資料傳輸方式,TCP協定确認後向不可靠的IP協定賦予可靠性
應用層
選擇資料傳輸路徑、資料确認過程都被吟唱到套接字内部,程式員無需考慮這些過程,大家隻需利用套接字編出程式即可。編寫軟體的過程中,需要根據程式特點決定伺服器端和用戶端之間的資料傳輸規則(方式),這便是應用層協定。網絡程式設計的大部分内容就是設計并實作應用層協定
實作基于TCP的伺服器端/用戶端
TCP伺服器端的預設函數調用順序
- socket()。建立套接字
- bind()。配置設定套接字位址
- listen()。等待連接配接請求狀态
- accept()。允許連接配接
- rand()/write()。資料交換
- close()。斷開連接配接
進入等待連接配接請求狀态
調用listen函數進入等待連接配接請求狀态。隻有調用了listen函數,用戶端才能進入可發出連接配接請求的狀态。即,這時用戶端才能調用connect函數(若提前調用将發生錯誤)
#include <sys/socket.h>
int listen(int sock, int backlog);
成功時傳回0,失敗時傳回-1。sock:希望進入等待連接配接請求狀态的套接字檔案描述符,傳遞的描述符套接字參數成為伺服器端套接字(監聽套接字);backlog:連接配接請求隊列(Queue)的長度,若為5,則隊列長度為5,表示最多使5個連接配接請求進入隊列
listen函數的第二個參數值與伺服器端的特性有關,像頻繁接收請求的Web伺服器端至少應為15
“伺服器端處于等待連接配接請求狀态”是指,用戶端請求連接配接時,受理連接配接前一直使請求處于等待狀态
受理用戶端連接配接請求
調用listen函數後,若有新的連接配接請求,則應按順序受理。受理請求意味着進入可接受資料的狀态。而伺服器端套接字是做門衛的,是以進入這種狀态需要另一個套接字,但沒必要親自建立,由accept函數自動建立套接字,并連接配接到發起請求的用戶端
#include <sys/socket.h>
int accept(int sock, struct sockaddr* addr, socklen_t* addrlen);
成功時傳回建立的套接字檔案描述符,失敗時傳回-1。sock:伺服器套接字的檔案描述符;addr:儲存發起連接配接請求的用戶端位址資訊的變量位址值,調用函數後向傳遞來的位址變量參數填充用戶端位址資訊;address:第二個參數addr結構體的長度,但是存有長度的變量位址。函數調用完成後,該變量即被填入用戶端位址長度
accept函數受理連接配接請求等待隊列中待處理的用戶端連接配接請求。函數調用成功時,accept函數内部将産生用于資料I/O的套接字,并傳回其檔案描述符
hello world伺服器端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unisted.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char *message);
int main(int argc, char* argv[])
{
int serv_sock;
int clnt_sock;
struct sockaddr_in serv_addr;
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size;
char message[] = "Hello World!";
if (argc != 2)
{
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0); // 建立套接字,此時套接字尚非真正的伺服器端套接字
if (serv_sock == -1)
{
error_handling("socket() error");
}
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
if (bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
{
error_handling("bind() error");
}
if (listen(serv_sock, 5) == -1) // 此時套接字才是伺服器端套接字
{
error_handling("listen() error");
}
clnt_addr_size = sizeof(clnt_addr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size); // 從隊頭取1個連接配接請求與用戶端建立連接配接,并傳回建立的套接字檔案描述符
if (clnt_sock == -1)
{
error_handling("accept() error");
}
write(clnt_sock, message, sizeof(message));
close(clnt_sock);
close(serv_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
TCP用戶端的預設函數調用順序
- socket()。建立套接字
- connect()。請求連接配接
- read()/write()。交換資料
- close()。斷開連接配接
伺服器端調用listen函數後建立連接配接請求等待隊列,之後用戶端即可請求連接配接。使用connect函數發起連接配接請求
#include <sys/socket.h>
int connect(int sock, struct sockaddr* servaddr, socklen_t addrlen);
成功時傳回0,失敗時傳回-1。sock:用戶端套接字檔案描述符;servaddr:儲存目标伺服器端位址資訊的變量位址值;addrlen:以位元組為機關傳遞已傳遞給第二個結構體參數servaddr的位址變量長度
用戶端調用connect函數後,發生以下情況之一才會傳回(完成函數調用):
- 伺服器端接收連接配接請求
- 發生斷網等異常情況而中斷連接配接請求
所謂的“接收連接配接”并不意味着伺服器端調用accept函數,其實是伺服器端把連接配接請求資訊記錄到等待隊列。是以connect函數傳回後并不立即進行資料交換
用戶端套接字的位址資訊
用戶端實作過程中并未出現套接字位址配置設定,而是建立套接字後立即調用connect函數。因為用戶端的IP位址和端口在調用connect函數時自動配置設定,無需調用标記的bind函數進行配置設定
何時:調用connect函數時;何地:作業系統核心中;如何:IP用計算機(主機)的IP,端口随機
Hello world用戶端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char *message);
int main(int argc, char* argv[])
{
int sock;
struct sockaddr_in serv_addr;
char message[30];
int str_len;
if (argc != 3)
{
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock == -1)
{
error_handling("socket() error");
}
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
if (connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) // 調用connect函數向伺服器端發送連接配接請求
{
error_handling("connect() error!");
}
str_len = read(sock, message, sizeof(message) - 1); // 完成連接配接後,接收伺服器端傳輸的資料
if (str_len == -1)
{
error_handling("read() error!");
}
printf("Message from server : %s \n", message);
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
基于TCP的伺服器端/用戶端函數調用關系
伺服器端建立套接字後連續調用bind、listen函數進入等待狀态,用戶端通過調用connect函數發起連接配接請求。需要注意的是,用戶端隻能等到伺服器端調用listen函數後才能調用connect函數。同時,用戶端調用connect函數前,伺服器端有可能率先調用accept函數,此時伺服器端在調用accept函數時進入阻塞(blocking)狀态,直到用戶端調用connect函數為止
實作疊代伺服器端/用戶端
實作疊代伺服器端
如果伺服器端處理完1個用戶端連接配接請求即退出,連接配接請求等待隊列實際沒有太大意義。實際上,伺服器端應該在設定好等待隊列的大小後,應向所有用戶端提供服務。如果想繼續受理後續的用戶端連接配接請求,最簡單的辦法就是插入循環語句反複調用accept函數,調用順序如下:
調用accept函數後,緊接着調用I/O相關的read、write函數,然後調用針對用戶端的close函數
疊代回聲伺服器端/用戶端
回聲(echo)服務端/用戶端,即伺服器端将用戶端傳輸的字元串資料原封不動地傳回用戶端,就像回聲一樣
程式的基本運作方式:
- 伺服器端在同一時刻隻與一個用戶端相連,并提供回聲服務
- 伺服器端依次向5個用戶端提供服務并退出
- 用戶端接收使用者輸入的字元串并發送到伺服器端
- 伺服器端将接收的字元串資料傳回用戶端,即“回聲”
- 伺服器端與用戶端之間的字元串回聲一直執行到用戶端輸入Q為止
回聲伺服器端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
void error_handling(char* message);
int main(int argc, char* argv[])
{
int serv_sock, clnt_sock;
char message[BUF_SIZE];
int str_len, i;
struct sockaddr_in serv_adr, clnt_adr;
socklen_t clnt_adr_sz;
if (argc != 2)
{
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if (serv_sock == -1)
{
error_handling("socket() error");
}
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
if (bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
{
error_handling("bind() error");
}
if (listen(serv_sock, 5) == -1)
{
error_handling("listen() error");
}
clnt_adr_sz = sizeof(clnt_adr);
for(i = 0; i < 5; i++) // 為處理5個用戶端連接配接而添加的循環語句
{
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
if (clnt_sock == -1)
{
error_handling("accept() error");
}
else
{
printf("Connected client %d \n", i+1);
}
while ((str_len = read(clnt_sock, message, BUF_SIZE)) != 0) // 原封不動地傳輸讀取的字元串,如果收到EOF則說明用戶端斷開連接配接,退出循環
{
write(clnt_sock, message, str_len);
}
close(clnt_sock); // 針對套接字調用close函數,向連接配接的相應套接字發送EOF
}
close(serv_sock);
return 0;
}
void error_handling(char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
回聲用戶端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
void error_handling(char* message);
int main(int argc, char* argv[])
{
int sock;
char message[BUF_SIZE];
int str_len;
struct sockaddr_in serv_adr;
if (argc != 3)
{
printf("Usage: %s <IP> <prot>\n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock == -1)
{
error_handling("socket() error");
}
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
serv_adr.sin_port = htons(atoi(argv[2]));
if (connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
{
error_handling("connect() error!");
}
else
{
puts("Connected..........");
}
while(1)
{
fputs("Input message(Q to quit): ", stdout);
fgets(message, BUF_SIZE, stdin);
if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
{
break;
}
write(sock, message, strlen(message)); // 以字元串為機關傳遞資料
str_len = read(sock, message, BUF_SIZE-1);
message[str_len] = 0;
printf("Message from server: %s", message);
}
close(sock); // 調用close函數向相應套接字發送EOF
return 0;
}
void error_handling(char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
回聲用戶端存在的問題
每次調用read、write函數時都會以字元串為機關執行實際的I/O操作。由于TCP不存在資料邊界,是以,多次調用write函數傳遞的字元串有可能一次性傳遞到伺服器端。此時用戶端有可能從伺服器端收到多個字元串
還有可能是,伺服器端希望通過1次write函數傳遞資料,但如果資料太大,作業系統就有可能把資料分成多個資料包發送到用戶端。另外,在此過程中,用戶端有可能在尚未收到全部資料包時就調用read函數
基于Windows實作
将Linux平台下的示例轉化成Windows平台示例的要點:
- 通過WSAStartup、WSACleanup函數初始化并清除套接字相關庫
- 把資料類型和變量名切換為Windows風格
- 資料傳輸中recv、send函數而非read、write函數
- 關閉套接字時用closesocket函數而非close函數
實作回聲伺服器端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <WinSock2.h>
#define BUF_SIZE 1024
void ErrorHandling(char* message);
int main(int argc, char* argv[])
{
WSADATA wsaData;
SOCKET hServSock, hClntSock;
char message[BUF_SIZE];
int strLen, i;
SOCKADDR_IN servAdr, clntAdr;
int clntAdrSize;
if (argc != 2)
{
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
ErrorHandling("WSAStartup() error!");
}
hServSock = socket(PF_INET, SOCK_STREAM, 0);
if (hServSock == INVALID_SOCKET)
{
ErrorHandling("socket() error");
}
memset(&servAdr, 0, sizeof(servAdr));
servAdr.sin_family = AF_INET;
servAdr.sin_addr.s_addr = htonl(INADDR_ANY);
servAdr.sin_port = htons(atoi(argv[1]));
if (bind(hServSock, (SOCKADDR*)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)
{
ErrorHandling("bind() error");
}
if (listen(hServSock, 5) == SOCKET_ERROR)
{
ErrorHandling("listen() error");
}
clntAdrSize = sizeof(clntAdr);
for (i = 0; i < 5; i++)
{
hClntSock = accept(hServSock, (SOCKADDR*)&clntAdr, &clntAdrSize);
if (hClntSock == -1)
{
ErrorHandling("accept() error");
}
else
{
printf("Connected client %d \n", i + 1);
}
while ((strLen = recv(hClntSock, message, BUF_SIZE, 0)) != 0)
{
send(hClntSock, message, strLen, 0);
}
closesocket(hClntSock);
}
closesocket(hServSock);
WSACleanup();
return 0;
}
void ErrorHandling(char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
實作回聲用戶端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <WinSock2.h>
#include <WS2tcpip.h>
#define BUF_SIZE 1024
void ErrorHandling(char* message);
int main(int argc, char* argv[])
{
WSADATA wsaData;
SOCKET hSocket;
char message[BUF_SIZE];
int strLen;
SOCKADDR_IN servAdr;
if (argc != 3)
{
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
ErrorHandling("WSAStartup() error!");
}
hSocket = socket(PF_INET, SOCK_STREAM, 0);
if (hSocket == INVALID_SOCKET)
{
ErrorHandling("socket() error");
}
memset(&servAdr, 0, sizeof(servAdr));
servAdr.sin_family = AF_INET;
inet_pton(AF_INET, argv[1], &servAdr.sin_addr);
servAdr.sin_port = htons(atoi(argv[2]));
if (connect(hSocket, (SOCKADDR*)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)
{
ErrorHandling("connect() error!");
}
else
{
puts("Connected.............");
}
while (1)
{
fputs("Input message(Q to quit): ", stdout);
fgets(message, BUF_SIZE, stdin);
if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
{
break;
}
send(hSocket, message, strlen(message), 0);
strLen = recv(hSocket, message, BUF_SIZE - 1, 0);
message[strLen] = 0;
printf("Message from server: %s", message);
}
closesocket(hSocket);
WSACleanup();
return 0;
}
void ErrorHandling(char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}