1. 程式人生 > >基於TCP通訊的簡單服務端和客戶端程式

基於TCP通訊的簡單服務端和客戶端程式

背景

因為最近在研究網路相關的東西,因此經常要寫程式做實驗來驗證。主要是TCP通訊,因此就寫了個簡單的基於TCP通訊的小程式,方便以後要使用的時候能直接複用,省的還要各種谷歌、百度。

功能介紹

寫的很簡單,實現的就是客戶端讀取鍵盤輸入,傳送給服務端,服務端打印出該輸入。
因為只研究TCP通訊原理,就沒有再做其他的多執行緒併發之類的功能。

程式碼

1、先看服務端程式碼

/*服務端TCP程式一般流程
 *1、建立socket
 *2、繫結埠和ip
 *3、監聽socket
 *4、接收客戶端的請求
 *5、從緩衝區中讀取資料
*/

#include <stdio.h>
#include <string.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/un.h> #include <arpa/inet.h> #include <netinet/in.h> #define PORT 5000 #define IP "192.168.0.106" #define BACKLOG 5 int main() { int listen_fd; int accept_fd; char
buf[1024] = {0};//讀寫緩衝區 struct sockaddr_in server_addr; //需要獲取客戶端相關資訊 struct sockaddr_in client_addr; socklen_t client_len; client_len = sizeof(client_addr); //建立socket if ((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("socket"); return 1; } //繫結埠和ip
server_addr.sin_family = AF_INET; server_addr.sin_port = htons(PORT); server_addr.sin_addr.s_addr = inet_addr(IP); if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { perror("bind"); return 2; } //監聽socket if (listen(listen_fd, BACKLOG) < 0) { perror("listen"); return 3; } while(1) { //接收客戶端的請求 if ((accept_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len)) < 0) { perror("accept"); return 4; } //列印客戶端資訊 printf("connected with ip: %s: port:%d\n", inet_ntop(AF_INET, &client_addr.sin_addr, buf, 1024), ntohs(client_addr.sin_port)); while (1) { memset(buf, 0, sizeof(buf)); //從緩衝區中讀取資料 ssize_t size = read(accept_fd, buf, sizeof(buf) - 1); if (size > 0) printf("client send: %s\n", buf); else if (size == 0) { printf("read done!\n"); close(accept_fd); break; } else { perror("read"); close(accept_fd); break; } } } close(listen_fd); return 0; }

2、然後是客戶端程式碼

/*客戶端TCP程式一般流程
 *1、建立socket
 *2、向服務端發起連線
 *3、向緩衝區中寫入資料
*/

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#define PORT 5000
#define IP "192.168.0.106"

int main()
{
    int client_fd;
    char buf[1024] = {0};//讀寫緩衝區
    struct sockaddr_in client_addr;
    ssize_t size;

    //建立socket
    if ((client_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket");
        return 1;
    }

    //填充ip埠資訊
    client_addr.sin_family = AF_INET;
    client_addr.sin_port = htons(PORT);
    client_addr.sin_addr.s_addr = inet_addr(IP);

    //向服務端發起連線
    if (connect(client_fd, (struct sockaddr*)&client_addr, sizeof(client_addr)) < 0) {
        perror("connect");
        return 2;
    }

    while(1) {
        memset(buf, 0, sizeof(buf));
        //從標準輸入獲取字串
        if (!gets(buf)) {
            perror("gets");
            return 3;
        }
        if (strcmp(buf, "quit") == 0)
            break;
        if (write(client_fd, buf, strlen(buf)) != strlen(buf)) {
            perror("write");
            return 4;
        }
    }

    close(client_fd);
    return 0;
}

3、最後是一個簡單的Makefile

all:
    gcc server.c -o server
    gcc client.c -o client
clean:
    rm -rf server client

編譯後程序就能跑起來了,使用netstat命令檢視連線狀態如下:
這裡寫圖片描述
這裡寫圖片描述

客戶端退出後,服務端還是在監聽新連線的到來。

探討

1、對於服務端程式碼的accept函式
一般情況我們是將第二、三個入參置為空的,表示我們不關注客戶端資訊,如下

accept_fd = accept(listen_fd, NULL, NULL);

此處我們為了列印客戶端ip和埠,因此傳入了我們定義的結構體,用於獲取客戶端資訊。

2、關於服務端監聽ip,或者是說繫結的ip
一般我們服務端程式監聽全網段ip,即在呼叫bind函式時ip地址使用INADDR_ANY作為引數,如下,

server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

這兩種使用的區別我們可以通過netstat命令檢視,對比如下,
這裡寫圖片描述
這裡寫圖片描述
上面的圖為設定了具體監聽ip的程式,下面是使用INADDR_ANY為引數的程式。由此,我們程式設計時就需要根據環境上是否有多個ip,程式是否需要監聽所有網絡卡的埠來確定我們函式的引數設定。

3、客戶端程式一般不呼叫bind函式
在客戶端程式中我們一般不呼叫bind函式,因為我們其實一般並不關心客戶端使用什麼ip和什麼埠和服務端通訊,核心會替你選擇一個未被佔用的埠以及能和服務端通訊的ip來發起連線。但是作為客戶端,我們是否可以使用bind函式呢?答案是肯定的。考慮以下場景,如果我們想要指定客戶端連線從哪個ip出去,使用哪個埠,這時候就必須使用bind函數了,這也就是bind函式的作用。
我們將上面客戶端的程式稍微修改一下,增加bind操作。為了更好的看出bind的結果,我在環境上配了兩個ip,讓客戶端和服務端各在一個ip上進行通訊。我們配置客戶端使用5555號埠,繫結在ip:192.168.0.107上,如程式碼所示:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#define PORT 5000
#define IP "192.168.0.106"

int main()
{
    int client_fd;
    char buf[1024] = {0};//讀寫緩衝區
    struct sockaddr_in client_addr;
    ssize_t size;

    //建立socket
    if ((client_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket");
        return 1;
    }

    /**************************************************************************/
    //客戶端使用bind函式繫結ip和埠
    struct sockaddr_in client_bind;
    client_bind.sin_family = AF_INET;
    client_bind.sin_port = htons(5555);
    client_bind.sin_addr.s_addr = inet_addr("192.168.0.107");
    if (bind(client_fd, (struct sockaddr*)&client_bind, sizeof(client_bind)) < 0) {
            perror("bind");
            return 5;
    }
    /**************************************************************************/

    //填充ip埠資訊
    client_addr.sin_family = AF_INET;
    client_addr.sin_port = htons(PORT);
    client_addr.sin_addr.s_addr = inet_addr(IP);

    //向服務端發起連線
    if (connect(client_fd, (struct sockaddr*)&client_addr, sizeof(client_addr)) < 0) {
        perror("connect");
        return 2;
    }

    while(1) {
        memset(buf, 0, sizeof(buf));
        //從標準輸入獲取字串
        if (!gets(buf)) {
            perror("gets");
            return 3;
        }
        if (strcmp(buf, "quit") == 0)
            break;
        if (write(client_fd, buf, strlen(buf)) != strlen(buf)) {
            perror("write");
            return 4;
        }
    }

    close(client_fd);
    return 0;
}

編譯執行後同樣使用netstat命令觀察連線狀態,如下:
這裡寫圖片描述
對比之前的截圖我們可以發現,客戶端的ip和埠已經變成我們設定的值。所以說,bind在客戶端也是可以使用的。