socket程式設計實現tcp伺服器_C/C++

雲夢士發表於2022-05-03

1. 需求分析

實現一個回聲伺服器的C/S(客戶端client/伺服器server)程式,功能為客戶端連線到伺服器後,傳送一串字串,伺服器接受資訊後,返回對應字串的大寫形式給客戶端顯示。
例如:

客戶端傳送“this is a webserver example!",

伺服器返回"THIS IS A WEBSERVER EXAMPLE!"

2. 專案實現

2.1 伺服器端程式echo_server.c

#include <stdio.h>      //printf
#include <stdlib.h>     //exit
#include <unistd.h>     //read, write, close
#include <sys/types.h>  //socket, bind, listen, accept
#include <sys/socket.h> //socket, bind, listen, accept
#include <string.h>     //strerror
#include <ctype.h>      //inet_ntop
#include <arpa/inet.h>  //inet_ntop
#include <errno.h>      //strerror

#define SERVER_PORT 666

//出錯處理
void perror_exit(const char* des) {
    fprintf(stderr, "%s error, reason: %s\n", des, strerror(errno));
    exit(1);
}

int main(void){

    int sock;//代表信箱
    int ret;//作為bind和listen的返回值,用於處理出錯資訊
    struct sockaddr_in server_addr;


    //1.建立套嵌字(信箱)。成功:返回socket的檔案描述符,失敗:返回-1,設定errno
    sock = socket(AF_INET, SOCK_STREAM, 0);
    if(sock == -1) {
        perror_exit("create socket");
    }

    //2.清空伺服器地址空間(標籤),寫上地址和埠號
    bzero(&server_addr, sizeof(server_addr));

    server_addr.sin_family = AF_INET;//選擇協議族IPV4
    //inet_pton(AF_INET, "1.1.1.1", &server_addr.sin_addr.s_addr);//測試出錯處理函式perror_exit
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//監聽本地所有IP地址
    server_addr.sin_port = htons(SERVER_PORT);//繫結埠號

    //3. 實現標籤貼到收信得信箱上
    ret = bind(sock, (struct sockaddr *)&server_addr,  sizeof(server_addr));
    if(ret == -1) {
        perror_exit("bind");
    }

    //4. 把信箱掛置到傳達室,這樣,就可以接收信件了(監聽客戶端)
    ret = listen(sock, 128);
    if(ret == -1) {
        perror_exit("listen");
    }

    //萬事俱備,只等來信
    printf("等待客戶端的連線\n");

    //5. 處理客戶端請求
    int done =1;
    while(done){
        struct sockaddr_in client;
        int client_sock, len, i;
        char client_ip[64];
        char buf[256];

        socklen_t  client_addr_len;
        client_addr_len = sizeof(client);
        client_sock = accept(sock, (struct sockaddr *)&client, &client_addr_len);

        //列印客服端IP地址和埠號
        printf("client ip: %s\t port : %d\n",
                 inet_ntop(AF_INET, &client.sin_addr.s_addr,client_ip,sizeof(client_ip)),
                 ntohs(client.sin_port));
        /*讀取客戶端傳送的資料*/
        len = read(client_sock, buf, sizeof(buf)-1);
        buf[len] = '\0';
        printf("receive[%d]: %s\n", len, buf);

        //轉換成大寫
        for(i=0; i<len; i++){
            buf[i] = toupper(buf[i]);
        }


        len = write(client_sock, buf, len);

        printf("finished. len: %d\n", len);
        close(client_sock);

    }
    
    //6. 關閉連線
    close(sock);
    return 0;
}

2.2 客戶端程式echo_client.c

#include <stdio.h>      //printf
#include <stdlib.h>     //exit
#include <string.h>     //memset, strlen
#include <unistd.h>     //read, write, close
#include <sys/socket.h> //socket, connect
#include <arpa/inet.h>  //inet_pton

#define SERVER_PORT 666
#define SERVER_IP  "127.0.0.1"
int main(int argc, char *argv[]){//argc表示傳入命令的個數,argv表示傳入的具體資訊

    int sockfd;
    char *message;
    struct sockaddr_in servaddr;
    int n;
    char buf[64];

    //異常處理
    if(argc != 2){
        fputs("Usage: ./echo_client message \n", stderr);
        exit(1);
    }

    message = argv[1];//傳入的資訊
    printf("message: %s\n", message);

    //1. 建立套嵌字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);

    memset(&servaddr, '\0', sizeof(struct sockaddr_in));//分配空間

    //定義地址IP和埠
    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET, SERVER_IP, &servaddr.sin_addr);
    servaddr.sin_port = htons(SERVER_PORT);

    //2. 連線伺服器
    connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

    //3. 讀寫和伺服器的互動資訊
    write(sockfd, message, strlen(message));

    n = read(sockfd, buf, sizeof(buf)-1);

    if(n>0){
        buf[n]='\0';
        printf("receive: %s\n", buf);
    }else {
        perror("error!!!");
    }

    printf("finished.\n");

    //4. 關閉連線
    close(sockfd);

    return 0;
}

3. 程式執行方式

我的echo_server.c程式在/share/echo_server資料夾下,echo_client.c程式在/share/echo_client資料夾下。
必須先執行伺服器程式,再執行客戶端程式。順序不能反!!

3.1. 執行伺服器程式

  • 首先,進入echo_server.c所在資料夾
    root@lxb-virtual-machine:/# cd /share/echo_server
  • 之後,編譯程式
    root@lxb-virtual-machine:/share/echo_server# gcc echo_server.c -o echo_server
  • 最後執行程式
  • root@lxb-virtual-machine:/share/echo_server# ./echo_server

全過程截圖:
image

3.2. 執行客戶端程式

  • 首先,進入echo_client.c所在資料夾
    root@lxb-virtual-machine:/# cd /share/echo_client
  • 之後,編譯程式
    root@lxb-virtual-machine:/share/echo_client# gcc echo_client.c -o echo_client
  • 最後執行程式
  • root@lxb-virtual-machine:/share/echo_client# ./echo_client "this is a webserver example!"

全過程截圖:
image

4. 分析

首先我們進行感性的分析,用來理解各個步驟的用意。之後我們需要對裡面涉及到的函式進行具體的分析。

4.1 李華寫信模型

我們在高中英語經常遇到的一道作文題就是“你是李華,請給國外的筆友Andy寫信”,而我們網路通訊也可以類比於“李華與國外筆友通訊”的模型。這裡我們將“李華”比作客戶端,“國外筆友Andy”作為伺服器端。

4.1.1 Andy應該怎麼做呢?

為了使“李華同學”與“國外筆友”能夠交流,首先需要統一語言,同時約定好寄信方式,郵局寄信還是電子郵件之類的,(這就是“socket套嵌字”)。之後Andy準備好一個信箱,之後找一張標籤紙(server_addr),整理乾淨這張標籤紙(bzero函式),往上面寫上自己的地址和門牌號,一切準備好後,將貼好標籤紙的信箱掛到外面(listen函式),這樣大家都能給Andy寄信。最後Andy只需要時不時去看看信箱有沒有信,有的話把信的內容讀出來(read函式),之後再寫封回信寄回去(write函式)。

4.1.2 李華應該怎麼做?

李信作為寫信人就比較簡單了,首先還是使用統一的寄信方式,往信封上寫上自己要寄的地址和門牌號,也就是Andy家的地址和門牌號,之後與Andy聯絡上(connect函式)。接下來就可以給Andy寫信(write函式),讀Andy的回信(read函式)。收到回信,不想再和Andy通訊了,這時就把兩個人的聯絡斷開(close函式)。

4.2 程式流程圖

image

4.3 具體函式解析

4.3.1 socket函式

  1. 所屬標頭檔案

    #include <sys/socket.h>

  2. 函式定義

    int socket(int domain, int type, int protocol);

  3. 引數含義

    • domain:
      AF_INET 這是大多數用來產生socket的協議,使用TCP或UDP來傳輸,用IPv4的地址
      AF_INET6 與上面類似,不過是來用IPv6的地址
      AF_UNIX 本地協議,使用在Unix和Linux系統上,一般都是當客戶端和伺服器在同一臺及其上的時候使用
    • type:
      SOCK_STREAM 這個協議是按照順序的、可靠的、資料完整的基於位元組流的連線。這是一個使用最多的socket型別,這個socket是使用TCP來進行傳輸。
      SOCK_DGRAM 這個協議是無連線的、固定長度的傳輸呼叫。該協議是不可靠的,使用UDP來進行它的連線。
      SOCK_SEQPACKET該協議是雙線路的、可靠的連線,傳送固定長度的資料包進行傳輸。必須把這個包完整的接受才能進行讀取。
      SOCK_RAW socket型別提供單一的網路訪問,這個socket型別使用ICMP公共協議。(ping、traceroute使用該協議)
      SOCK_RDM 這個型別是很少使用的,在大部分的作業系統上沒有實現,它是提供給資料鏈路層使用,不保證資料包的順序
    • protocol:
      傳0 表示使用預設協議。
    • 返回值
      成功:返回指向新建立的socket的檔案描述符,失敗:返回-1,設定errno

4.3.2 bind函式

  1. 所屬標頭檔案

    #include <sys/socket.h>

  2. 函式定義

    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

  3. 引數含義

    • sockfd:
      socket檔案描述符
    • addr:
      IP地址加埠號
    • addrlen:
      addr的長度
    • 返回值
      成功:返回0,失敗:返回-1,設定errno

4.3.3 listen函式

  1. 所屬標頭檔案

    #include <sys/socket.h>

  2. 函式定義

    int listen(int sockfd, int backlog);

  3. 引數含義

    • sockfd:
      socket檔案描述符
    • backlog:
      在Linux 系統中,它是指排隊等待建立3次握手佇列長度
    • 返回值
      成功:返回0,失敗:返回-1,設定errno

4.3.4 accept函式

  1. 所屬標頭檔案

    #include <sys/socket.h>

  2. 函式定義

    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

  3. 引數含義

    • sockfd:
      socket檔案描述符
    • addr:
      IP地址加埠號
    • addrlen:
      addr的長度
    • 返回值
      成功:返回一個新的socket檔案描述符,失敗:返回-1,設定errno

4.3.5 connect函式

  1. 所屬標頭檔案

    #include <sys/socket.h>

  2. 函式定義

    int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

  3. 引數含義

    • sockfd:
      socket檔案描述符
    • addr:
      IP地址加埠號
    • addrlen:
      addr的長度
    • 返回值
      成功:返回一個新的socket檔案描述符,失敗:返回-1,設定errno

4.3.6 出錯處理函式

  1. 所屬標頭檔案

    #include <errno.h>
    #include <string.h>
    
  2. 函式定義

    char *strerror(int errnum);

  3. 引數含義

    • errnum:
      錯誤編號的值,一般取 errno 的值
    • 返回值
      錯誤原因

5.感謝

感謝bilibili的Martin老師的視訊: C語言/C++伺服器開發】小白實現第一個伺服器入門專案 網路通訊與Socket 程式設計詳解&原始碼分享,本篇部落格也是基於Martin老師這個視訊所做的。

相關文章