1. 程式人生 > >套接字程式設計的基本模式(網路程式設計Linux_C -> 筆記二)

套接字程式設計的基本模式(網路程式設計Linux_C -> 筆記二)

套接字程式設計的基本模式

客戶端/伺服器

  客戶端/伺服器模式就是基本的網路程式設計模式,簡稱C/S(即Client/Server)模式。需要注意的是這裡的客戶端、伺服器指的是軟體層面的意思而不是硬體,即客戶端、伺服器是分別執行在兩臺電腦上的兩個軟體。

  一般而言,伺服器的IP地址和埠號是要眾所周知的,並且伺服器要24小時不間斷執行;相反,客戶端的IP地址和埠號就沒有眾所周知的要求,也沒有不間斷執行的要求。這裡的原因很簡單,就是因為伺服器是被訪問方,大家都要知道它的地址才能訪問它,並且它不知道什麼時候有人會訪問,因此要一直執行。

一些與客戶端有關的基本函式

  與客戶端有關的基本函式如下:

#include
<sys/socket.h>
int socket(int family, int type, int protocol); /* 返回值:若呼叫成功,則返回對應的套接字描述符;若出錯則返回-1 */ int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen); /*返回值:若成功返回0,若出錯則返回-1*/

  其中socket在筆記一中已經說明了。

  connect函式用於客戶端嘗試對伺服器進行連線。其中sockfd是客戶端的套接字描述符;servaddr是指向伺服器套接字地址結構的指標;addrlen是伺服器套接字地址結構的指標。

  在這裡一定要分清,三個引數分別描述的是客戶端的屬性還是伺服器的屬性。很清楚的是,客戶端不需要提供自己的地址,所以servaddr和addrlen引數描述的一定是與伺服器相關的屬性,指明瞭客戶端到底要和哪個伺服器通訊。   而第一個引數sockfd則指明客戶端要用自己的哪一個套接字和伺服器通訊。(沒錯,一個程序就是可以建立多個套接字,就像一個程序可以建立多個檔案一樣)

一些與伺服器有關的基本函式

  與伺服器有關的基本函式如下:

#include <sys/socket.h>

int socket(int family, int type, int protocol);
/* 返回值:若呼叫成功,則返回對應的套接字描述符;若出錯則返回-1 */
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen); /*返回值:若成功返回0,若出錯則返回-1*/ int listen(int sockfd, int backlog); /*返回值:若成功返回0,若出錯則返回-1*/ int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen); /*返回值:若成功則為非負描述符,若出錯則為-1*/

  同樣地,socket省略不講了。

  bind函式用於將一個套接字繫結上一個地址結構。之前說過伺服器的地址是眾所周知的,所以它的套接字地址必須明確。其中sockfd是用於繫結的套接字;myaddr就是指向伺服器地址結構的指標(該地址結構將被用於繫結);addrlen就是該地址結構的大小。

客戶端也可以用bind函式來繫結套接字與地址結構,但沒有必要,因為客戶端的地址不需要眾所周知。如果套接字不繫結地址,系統會自動為套接字分配一個空閒的地址結構。

  listen函式用於監聽連線,在TCP協議中通訊前需要先建立一個邏輯連線,listen函式也僅由TCP伺服器呼叫。其中sockfd是伺服器的監聽套接字,backlog是伺服器在同一時刻最多可以監聽多少個連線。

  首先伺服器往往不會只有一個套接字。伺服器的套接字中一般被分為監聽套接字和已連線套接字。其中監聽套接字專門用來作為listen函式的引數來監聽有沒有客戶端要連線它,隨後會呼叫accept函式(下面會講)建立一個已連線套接字,已連線套接字才是伺服器真正用於和客戶端通訊的套接字(所以我覺得已連線套接字叫作通訊套接字會更好理解些…)。   其次要注意的是,呼叫listen函式並不會使伺服器進入阻塞(即使沒有連線請求)。listen只是做了一個初始化的作用,用來告訴核心某個套接字將會被作為監聽套接字用來接收連線請求,隨後一旦有連線請求該監聽套接字會被核心自動更新。而建立連線(所謂的TCP三次握手)是由accept來完成的,如果accept呼叫無法從監聽套接字中讀到任何內容(即沒有連線請求),就會使伺服器進入阻塞態直到有請求出現為止。   其次,關於listen函式的backlog暫時沒有去過多的理解,在練習的時候一般設為5都是足夠的。但是根據《UNIX網路程式設計》的原話,如果不想讓任何客戶連線到你的監聽套接字上,應該將監聽套接字關閉(怎麼關閉?檔案怎麼關閉的套接字就怎麼關閉)而不是把backlog設為0,backlog定義為0的後果是無法確定的。

  accept函式用於建立一個已連線套接字。其中第一個引數sockfd是監聽套接字;第二個引數和第三個引數被稱為 “值-結果” 引數,雖然accept的返回值只有一個(返回一個新的套接字描述符,即已連線套接字描述符),但是accept還會將客戶端套接字的地址結構和長度返回並存放在第二個和第三個引數中。

所謂的 值-結果 引數,個人的理解就是,該類引數不僅可以作為值傳遞給一個函式,還可以存放呼叫函式後的結果。因為函式的返回值只有一個,所以要返回額外的值就可以存放在引數中。

TCP型客戶端-伺服器的通訊示意圖

  用一張圖來描述呼叫函式的過程應該會好理解一些: 在這裡插入圖片描述

TCP通訊示例程式碼

// 伺服器的程式碼
// FILENAME: server.c
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(int argc, char **argv)
{
    int lis_sockfd, conn_sockfd; /*宣告:監聽套接字和連線套接字的描述符*/
    struct sockaddr_in server_address, client_address; /*宣告:伺服器和客戶端的套接字地址結構*/
    int server_len, client_len; /*宣告:套接字地址結構的長度*/
    
    lis_sockfd = socket(AF_INET, SOCK_STREAM, 0); /*建立監聽套接字*/

	/*為伺服器套接字地址結構的各個成員進行賦值*/
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = inet_addr("127.0.0.1");
    server_address.sin_port = htons(4000);
    
    server_len = sizeof(server_address);/*獲取伺服器地址結構的長度*/
    
    bind(listen_sockfd, (struct sockaddr *)&server_address, server_len); /*為監聽套接字繫結眾所周知的地址*/

    listen(lis_sockfd, 5); /*將lis_sockfd正式宣告為監聽套接字*/
    
    while(1){
        char ch;
        printf("server waiting\n");
        client_len = sizeof(client_address);
        conn_sockfd = accept(lis_sockfd, (struct sockaddr *)&client_address, (socklen_t *)&client_len); /*建立連線*/
        read(conn_sockfd, &ch, 1);
        ch++;
        write(conn_sockfd, &ch, 1);
        close(conn_sockfd);
    }
    exit(0);
}
//客戶端程式碼
//FILENAME: client.c
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(int argc, char **argv)
{
    int client_sockfd;
    int server_len;
    struct sockaddr_in server_address;
    
    int result;
    char ch;

    client_sockfd = socket(AF_INET, SOCK_STREAM, 0);

    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = inet_addr("127.0.0.1");
    server_address.sin_port = htons(4000);
    server_len = sizeof(server_address);
    
    result = connect(client_sockfd, (struct sockaddr *)&server_address, server_len);

    if(result == -1){
        perror("oops: client");
        exit(1);
    }

    while(1)
    {
        ch = getchar();
        write(client_sockfd, &ch, 1);
        read(client_sockfd, &ch, 1);
        printf("char from server = %c\n", ch);
        break;
    }
    close(client_sockfd);
    exit(0);
}

127.0.0.1代表的是自己的主機地址。 另外,"127.0.0.1"計算機是看不懂的,inet_addr函式將其轉化成計算機能看懂的形式。 上面的伺服器程式碼,如果不強制退出伺服器它是永遠不會被關閉的。