風起於青萍之末,浪成於微瀾之間,每個龐大複雜的系統,皆起始於一個最簡單的,最細微的地方。
從這裏開始,我將會從一個最基本的服務器開始梳理,用於夯實自己的基礎。
這裏所講述的一切服務器內容,皆始於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
上面的ip地址爲127.0.0.1,服務器端口號爲20000,客戶端端口號爲55233,你可以在16進制中分別找到他們,點分十進制的127.0.0.1轉換爲十進制是2130706433,再轉換爲16進製爲7f000001,20000的十六進制爲4e20,客戶端的55233十六進制爲d7c1,你都可以分別在上面的數據包中找到每個協議對應的信息。
我開始寫自己的公衆號了,如果你對服務器有興趣,歡迎關注呀~