C 語言實現一個簡單的 web 服務器

說到 web 服務器想必大多數人首先想到的協議是 http,那麼 http 之下則是 tcp,本篇文章將經過 tcp 來實現一個簡單的 web 服務器。html

來源:公衆號(c語言與cpp編程)web

本篇文章將着重講解如何實現,對於 http 與 tcp 的概念本篇將不過多講解。編程

1、瞭解 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 將數據進行返回。服務器

2、建立sokect完成監聽

2.1 WSAStartup初始化網絡

首先在c語言頭文件中引入依賴 WinSock2.h:socket

include <WinSock2.h>

在第一點中對 socket 的建立步驟已有說明,首先須要完成 socket 的初始化操做,使用函數 WSAStartup,該函數的原型爲:tcp

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 OKrn";
int len;
char ctype[30] = "Content-type:text/htmlrn";
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, "rn", 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>

//來源:公衆號(c語言與cpp編程)

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 OKrn";
int len;
char ctype[30] = "Content-type:text/htmlrn";
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, "rn", 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。

相關文章
相關標籤/搜索