從零開始構建一個服務器【1】

風起於青萍之末,浪成於微瀾之間,每個龐大複雜的系統,皆起始於一個最簡單的,最細微的地方。

從這裏開始,我將會從一個最基本的服務器開始梳理,用於夯實自己的基礎。

這裏所講述的一切服務器內容,皆始於Linux操作系統之上,因爲現在的主流服務器,很少搭建於其他的環境之上。

大多服務器,都會都會遵循如下的一些基本特點,首先,用一個socket接口來創建一個文件描述符file descriptor,畢竟Linux之下,萬物皆文件。用這個套接字來告訴操作系統,你用的是什麼網絡協議,如UDP,或TCP,協議家族屬於誰。一個進程可以創建的的fd可以不止一個。

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

然後將這個socket的返回值,也就是一個文件描述符,綁定在一個二元組上,這個二元組就是一個ip地址和port(端口號)。根據這個IP地址和端口號,唯一標識一個進程。IP地址標識的是一個主機,port標識的是一個進程。這個所用的系統調用叫做bind

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

不過現代互聯網中,其實這並不能使我們找到一個唯一的進程,因爲IPv4協議之下,公網的IP地址早已經不夠用了。因此我們採用了很多技術手段來使得我們的進程能夠被公網所訪問,如子網掩碼等。但我們必須有一個公網所能夠訪問的IP地址。

bind函數的第二個參數是一個由於歷史原因設計的,使得我們使用不夠靈活的參數,因此,當我們填寫這個參數的時候,通常會使用struct sockaddr_in這個結構體。

接下來,我們使用listen函數來保持對外監聽。當有新連接來的時候,我們只需要接收它就可以了。

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

在使用這四個系統調用,我們需要使用如下頭文件:

#include <sys/types.h> 
#include <sys/socket.h>

而struct sockaddr_in這個結構體,需要包含如下頭文件:

#include <arpa/inet.h>

接下來,讓我們編寫一個簡單的服務器:

#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <iostream>
#include <cstdio>

using namespace std;

int main()
{
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd < 0)
    {
        cerr << "socket" << endl;
        return 1;
    }
    struct sockaddr_in bindaddr;
    bindaddr.sin_family = AF_INET;  //這是選擇協議家族,AF_INET屬於IPv4
    
    //這裏的這個INADDR_ANY宏代表我們並不關心IP地址到底是多少,因爲一個主機可能有多個網卡
    //每個都有不用的地址,讓操作系統自己選擇就好
    bindaddr.sin_addr.s_addr = htonl(INADDR_ANY); 
    bindaddr.sin_port = htons(20000);

    //我們需要將二元組轉換爲bind函數接收的參數
    if(bind(fd, (struct sockaddr*)&bindaddr, sizeof(bindaddr)) < 0)
    {
        cerr << "bind" << endl;
        return 2;
    }

    if(listen(fd, SOMAXCONN) < 0)
    {
        cerr << "listen" << endl;
        return 3;
    }

    while(true)
    {
        struct sockaddr_in peeraddr;
        socklen_t len = sizeof(peeraddr);
        int clientfd;
        //注意,第三個參數是指針,這是個輸出型參數,由操作系統返回
        if((clientfd = accept(fd, (struct sockaddr*)&peeraddr, &len)) < 0)
        {
            cerr << "accept" << endl;
            return 4;
        }
        char buf[128] = { 0 };
        int n = 0;
        //if((n = read(clientfd, buf, sizeof buf)) < 0)
        //這兩個調用,在這裏起到到的效果是一樣的
        if((n = recv(clientfd, buf, sizeof(buf), 0)) < 0)
        {
            cerr << "read" << endl;
            return 5;
        }
        buf[n] = '\0';
        //cout << buf << endl;
        printf("%s\n", buf);
        //if(write(clientfd, buf, n + 1) < 0)
        //這兩個調用,在這裏起到到的效果是一樣的
        if(send(clientfd, buf, sizeof(buf), 0) < 0)
        {
            cerr << "write" << endl;
            return 6;
        }
        memset(buf, 0, sizeof(buf));
        close(clientfd);//當我們完成與這個客戶端的交互之後,需要用close來關閉與這個客戶端的連接
    }

    return 0;
}

上面的服務器,我們可以用nc -v ip port的命令來對它進行連接,輸入字符後,服務器會原封不動的進行返回,字節數不超過127,服務器編寫的非常簡單,很多的差錯處理都不夠完善。

我們也可以編寫一個客戶端程序來連接我們的服務器:


#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <iostream>
#include <string>
#include <cstdio>

using namespace std;

int main()
{
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in localaddr;
    localaddr.sin_family = AF_INET;
    localaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    localaddr.sin_port = htons(20000);
    if(connect(fd, (struct sockaddr*)&localaddr, sizeof localaddr) < 0)
        return 1;
    char msg[] = "hello";
    char buf [128] = { 0 };
    write(fd, msg, strlen(msg));
    int n = read(fd, buf, sizeof buf);
    buf[n] = 0;
    printf("%s\n", buf);
    memset(buf, 0, sizeof buf);
    close(fd);
}

客戶端連上服務器後,會通過TCP協議,來傳輸一個字符串"hello",服務器會原封不動的返回給客戶端,構成一個簡單的回射服務器。

那麼如何驗證它是通過TCP協議來發送數據的呢?

我們可以用tcpdump命令來對數據進行抓包。

tcpdump -i any 'port 20000' -XX -nn -vv

TCP請求連接的報頭
上面的ip地址爲127.0.0.1,服務器端口號爲20000,客戶端端口號爲55233,你可以在16進制中分別找到他們,點分十進制的127.0.0.1轉換爲十進制是2130706433,再轉換爲16進製爲7f000001,20000的十六進制爲4e20,客戶端的55233十六進制爲d7c1,你都可以分別在上面的數據包中找到每個協議對應的信息。

我開始寫自己的公衆號了,如果你對服務器有興趣,歡迎關注呀~
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章