說到 web 服務器想必大多數人首先想到的協議是 http,那么 http 之下則是 tcp,本篇文章將通過 tcp 來實作一個簡單的 web 服務器,
本篇文章將著重講解如何實作,對于 http 與 tcp 的概念本篇將不過多講解,
一、了解 Socket 及 web 服務作業原理
既然是基于 tcp 實作 web 服務器,很多學習 C 語言的小伙伴可能會很快的想到套接字 socket,socket 是一個較為抽象的通信行程,或者說是主機與主機進行資訊互動的一種抽象,socket 可以將資料流送入網路中,也可以接收資料流,
socket 的資訊互動與本地檔案資訊的讀取從表面特征上看類似,但其中所存在的撰寫復雜度是本地 IO 不能比擬的,但卻有相似點,在 win 下 socket 的互動互動步驟為:WSAStartup 進行初始化--> socket 創建套接字--> bind 系結--> listen 監聽--> connect 連接--> accept 接收請求--> send/recv 發送或接收資料--> closesocket 關閉 socket--> WSACleanup 最終關閉,

了解完了一個 socket 的基本步驟后我們了解一下一個基本 web 請求的用戶常規操作,操作分為:打開瀏覽器-->輸入資源地址 ip 地址-->得到資源,當目標服務器接收到該操作產生掉請求后,我們可以把服務器的回應流程步驟看為:獲得 request 請求-->得到請求關鍵資料-->獲取關鍵資料-->發送關鍵資料,服務器的這一步流程是在啟動socket 進行監聽后才能回應,通過監聽得知接收到請求,使用 recv 接收請求資料,從而根據該引數得到進行資源獲取,最后通過 send 將資料進行回傳,
二、創建sokect完成監聽
2.1 WSAStartup初始化
首先在c語言頭檔案中引入依賴 WinSock2.h:
#include <WinSock2.h>
在第一點中對 socket 的創建步驟已有說明,首先需要完成 socket 的初始化操作,使用函式 WSAStartup,該函式的原型為:
int WSAStartup(
WORD wVersionRequired,
LPWSADATA lpWSAData
);
該函式的引數 wVersionRequired 表示 WinSock2 的版本號;lpWSAData 引數為指向 WSADATA 的指標,WSADATA 結構用于 WSAStartup 初始化后回傳的資訊,
wVersionRequired 可以使用 MAKEWORD 生成,在這里可以使用版本 1.1 或版本2.2,1.1 只支持 TCP/IP,版本 2.1 則會有更多的支持,在此我們選擇版本 1.1,
首先宣告一個 WSADATA 結構體 :
WSADATA wsaData;
隨后傳參至初始化函式 WSAStartup 完成初始化:
WSAStartup(MAKEWORD(1, 1), &wsaData)
WSAStartup 若初始化失敗則會回傳非0值:
if (WSAStartup(MAKEWORD(1, 1), &wsaData) != 0)
{
exit(1);
}
2.2 創建socket 套接字
初始化完畢后開始創建套接字,套接字創建使用函式,函式原型為:
SOCKET WSAAPI socket(
int af,
int type,
int protocol
);
在函式原型中,af 表示 IP 地址型別,使用 PF_INET 表示 IPV4,type 表示使用哪種通信型別,例如 SOCK_STREAM 表示 TCP,protocol 表示傳輸協議,使用 0 會根據前 2 個引數使用默認值,
int skt = socket(PF_INET, SOCK_STREAM, 0);
創建完 socket 后,若為 -1 表示創建失敗,進行判斷如下:
if (skt == -1)
{
return -1;
}
2.3 系結服務器
創建完 socket 后需要對服務器進行系結,配置埠資訊、IP 地址等, 首先查看 bind 函式需要哪一些引數,函式原型如下:
int bind(
SOCKET socket,
const sockaddr *addr,
int addrlen
);
引數 socket 表示系結的 socket,傳入 socket 即可;addr 為 sockaddr_in 的結構體變數的指標,在 sockaddr_in 結構體變數中配置一些服務器資訊;addrlen 為 addr 的大小值,
通過 bind 函式原型得知了我們所需要的資料,接下來創建一個 sockaddr_in 結構體變數用于配置服務器資訊:
struct sockaddr_in server_addr;
隨后配置地址家族為AF_INET對應TCP/IP:
server_addr.sin_family = AF_INET;
接著配置埠資訊:
server_addr.sin_port = htons(8080);
再指定 ip 地址:
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
ip 地址若不確定可以手動輸入,最后使用神器 memset 初始化記憶體,完整代碼如下:
//配置服務器
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
memset(&(server_addr.sin_zero), '\0', 8);
隨后使用 bind 函式進行系結且進行判斷是否系結成功:
//系結
if (bind(skt, (struct sockaddr *)&server_addr,sizeof(server_addr)) == -1) {
return -1;
}
2.4 listen進行監聽
系結成功后開始對埠進行監聽,查看 listen 函式原型:
int listen(
int sockfd,
int backlog
)
函式原型中,引數 sockfd 表示監聽的套接字,backlog 為設定內核中的某一些處理(此處不進行深入講解),直接設定成 10 即可,最大上限為 128,使用監聽并且判斷是否成功代碼為:
if (listen(skt, 10) == -1 ) {
return -1;
}
此階段完整代碼如下:
#include <WinSock2.h>
#include<stdio.h>
int main(){
//初始化
WSADATA wsaData;
if (WSAStartup(MAKEWORD(1, 1), &wsaData) != 0) {
exit(1);
}
//socket創建
int skt = socket(PF_INET, SOCK_STREAM, 0);
if (skt == -1) {
return -1;
}
//配置服務器
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
memset(&(server_addr.sin_zero), '\0', 8);
//系結
if (bind(skt, (struct sockaddr *)&server_addr,sizeof(server_addr)) == -1){
return -1;
}
//監聽
if (listen(skt, 10) == -1 ) {
return -1;
}
printf("Listening ... ...\n");
}
運行代碼可得知代碼無錯誤,并且輸出 listening:
![]()
在這里插入圖片描述
2.5 獲取請求
監聽完成后開始獲取請求,受限需要使用 accept 對套接字進行連接,accept 函式原型如下:
int accept(
int sockfd,
struct sockaddr *addr,
socklen_t *addrlen
);
引數 sockfd 為指定的套接字;addr 為指向 struct sockaddr 的指標,一般為客戶端地址;addrlen 一般設定為設定為 sizeof(struct sockaddr_in) 即可,代碼為:
struct sockaddr_in c_skt;
int s_size=sizeof(struct sockaddr_in);
int access_skt = accept(skt, (struct sockaddr *)&c_skt, &s_size);
接下來開始接受客戶端的請求,使用recv函式,函式原型為:
ssize_t recv(
int sockfd,
void *buf,
size_t len,
int flags
)
引數 sockfd 為 accept 建立的通信;buf 為快取,資料存放的位置;len 為快取大小;flags 一般設定為0即可:
//獲取資料
char buf[1024];
if (recv(access_skt, buf, 1024, 0) == -1) {
exit(1);
}
此時我們再到 accpt 和 recv 外層添加一個回圈,使之流程可重復:
while(1){
//建立連接
printf("Listening ... ...\n");
struct sockaddr_in c_skt;
int s_size=sizeof(struct sockaddr_in);
int access_skt = accept(skt, (struct sockaddr *)&c_skt, &s_size);
//獲取資料
char buf[1024];
if (recv(access_skt, buf, 1024, 0) == -1) {
exit(1);
}
}
并且可以在瀏覽器輸入 127.0.0.1:8080 將會看到客戶端列印了 listening 新建了鏈接:


我們添加printf陳述句可查看客戶端請求:
while(1){
//建立連接
printf("Listening ... ...\n");
struct sockaddr_in c_skt;
int s_size=sizeof(struct sockaddr_in);
int access_skt = accept(skt, (struct sockaddr *)&c_skt, &s_size);
//獲取資料
char buf[1024];
if (recv(access_skt, buf, 1024, 0) == -1) {
exit(1);
}
printf("%s",buf);
}
![]()
接下來我們對請求頭進行對應的操作,
2.6 請求處理層撰寫
得到請求后開始撰寫處理層,繼續接著代碼往下寫沒有層級,撰寫一個函式名為 req,該函式接收請求資訊與一個建立好的連接為引數:
void req(char* buf, int access_socket)
{
}
然后先在 while 回圈中傳遞需要的值:
req(buf, access_skt);
接著開始撰寫 req 函式,首先在 req 函式中標記當前目錄下:
char arguments[BUFSIZ];
strcpy(arguments, "./");
隨后分離出請求與引數:
char command[BUFSIZ];
sscanf(request, "%s%s", command, arguments+2);
接著我們標記一些頭元素:
char* extension = "text/html";
char* content_type = "text/plain";
char* body_length = "Content-Length: ";
接著獲取請求引數,若獲取 index.html,就獲取當前路徑下的該檔案:
FILE* rfile= fopen(arguments, "rb");
獲取檔案后表示請求 ok,我們先回傳一個 200 狀態:
char* head = "HTTP/1.1 200 OK\r\n";
int len;
char ctype[30] = "Content-type:text/html\r\n";
len = strlen(head);
接著撰寫一個發送函式 send_:
int send_(int s, char *buf, int *len)
{
int total;
int bytesleft;
int n;
total=0;
bytesleft=*len;
while(total < *len)
{
n = send(s, buf+total, bytesleft, 0);
if (n == -1)
{
break;
}
total += n;
bytesleft -= n;
}
*len = total;
return n==-1?-1:0;
}
send 函式功能并不難在此不再贅述,就是一個遍歷發送的邏輯,隨后發送 http 回應與檔案型別:
send_(send_to, head, &len);
len = strlen(ctype);
send_(send_to, ctype, &len);
隨后獲得請求檔案的描述,需要添加頭檔案#include <sys/stat.h>使用fstat,且向已連接的通信發生必要的資訊 :
//獲取檔案描述
struct stat statbuf;
char read_buf[1024];
char length_buf[20];
fstat(fileno(rfile), &statbuf);
itoa( statbuf.st_size, length_buf, 10 );
send(client_sock, body_length, strlen(body_length), 0);
send(client_sock, length_buf, strlen(length_buf), 0);
send(client_sock, "\n", 1, 0);
send(client_sock, "\r\n", 2, 0);
最后發送資料:
//·資料發送
char read_buf[1024];
len = fread(read_buf ,1 , statbuf.st_size, rfile);
if (send_(client_sock, read_buf, &len) == -1) {
printf("error!");
}
最后訪問地址 http://127.0.0.1:8080/index.html,得到當前目錄下 index.html 檔案資料,并且在瀏覽器渲染:

所有代碼如下:
#include <WinSock2.h>
#include<stdio.h>
#include <sys/stat.h>
int send_(int s, char *buf, int *len) {
int total;
int bytesleft;
int n;
total=0;
bytesleft=*len;
while(total < *len)
{
n = send(s, buf+total, bytesleft, 0);
if (n == -1)
{
break;
}
total += n;
bytesleft -= n;
}
*len = total;
return n==-1?-1:0;
}
void req(char* request, int client_sock) {
char arguments[BUFSIZ];
strcpy(arguments, "./");
char command[BUFSIZ];
sscanf(request, "%s%s", command, arguments+2);
char* extension = "text/html";
char* content_type = "text/plain";
char* body_length = "Content-Length: ";
FILE* rfile= fopen(arguments, "rb");
char* head = "HTTP/1.1 200 OK\r\n";
int len;
char ctype[30] = "Content-type:text/html\r\n";
len = strlen(head);
send_(client_sock, head, &len);
len = strlen(ctype);
send_(client_sock, ctype, &len);
struct stat statbuf;
char length_buf[20];
fstat(fileno(rfile), &statbuf);
itoa( statbuf.st_size, length_buf, 10 );
send(client_sock, body_length, strlen(body_length), 0);
send(client_sock, length_buf, strlen(length_buf), 0);
send(client_sock, "\n", 1, 0);
send(client_sock, "\r\n", 2, 0);
char read_buf[1024];
len = fread(read_buf ,1 , statbuf.st_size, rfile);
if (send_(client_sock, read_buf, &len) == -1) {
printf("error!");
}
return;
}
int main(){
WSADATA wsaData;
if (WSAStartup(MAKEWORD(1, 1), &wsaData) != 0) {
exit(1);
}
int skt = socket(PF_INET, SOCK_STREAM, 0);
if (skt == -1) {
return -1;
}
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
memset(&(server_addr.sin_zero), '\0', 8);
if (bind(skt, (struct sockaddr *)&server_addr,sizeof(server_addr)) == -1) {
return -1;
}
if (listen(skt, 10) == -1 ) {
return -1;
}
while(1){
printf("Listening ... ...\n");
struct sockaddr_in c_skt;
int s_size=sizeof(struct sockaddr_in);
int access_skt = accept(skt, (struct sockaddr *)&c_skt, &s_size);
char buf[1024];
if (recv(access_skt, buf, 1024, 0) == -1) {
exit(1);
}
req(buf, access_skt);
}
}
小伙伴們可以撰寫更加靈活的指定資源型別、錯誤處理等完善這個 demo,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/196757.html
標籤:python
上一篇:cgb2007-京淘day03
