天天看點

網絡程式設計——面向連接配接的伺服器端和用戶端的編寫

參考

  1. 《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伺服器端的預設函數調用順序

  1. socket()。建立套接字
  2. bind()。配置設定套接字位址
  3. listen()。等待連接配接請求狀态
  4. accept()。允許連接配接
  5. rand()/write()。資料交換
  6. 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用戶端的預設函數調用順序

  1. socket()。建立套接字
  2. connect()。請求連接配接
  3. read()/write()。交換資料
  4. close()。斷開連接配接

伺服器端調用listen函數後建立連接配接請求等待隊列,之後用戶端即可請求連接配接。使用connect函數發起連接配接請求

#include <sys/socket.h>

int connect(int sock, struct sockaddr* servaddr, socklen_t addrlen);
           

成功時傳回0,失敗時傳回-1。sock:用戶端套接字檔案描述符;servaddr:儲存目标伺服器端位址資訊的變量位址值;addrlen:以位元組為機關傳遞已傳遞給第二個結構體參數servaddr的位址變量長度

用戶端調用connect函數後,發生以下情況之一才會傳回(完成函數調用):

  1. 伺服器端接收連接配接請求
  2. 發生斷網等異常情況而中斷連接配接請求

所謂的“接收連接配接”并不意味着伺服器端調用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)服務端/用戶端,即伺服器端将用戶端傳輸的字元串資料原封不動地傳回用戶端,就像回聲一樣

程式的基本運作方式:

  1. 伺服器端在同一時刻隻與一個用戶端相連,并提供回聲服務
  2. 伺服器端依次向5個用戶端提供服務并退出
  3. 用戶端接收使用者輸入的字元串并發送到伺服器端
  4. 伺服器端将接收的字元串資料傳回用戶端,即“回聲”
  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平台示例的要點:

  1. 通過WSAStartup、WSACleanup函數初始化并清除套接字相關庫
  2. 把資料類型和變量名切換為Windows風格
  3. 資料傳輸中recv、send函數而非read、write函數
  4. 關閉套接字時用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);
}
           

繼續閱讀