|
说到 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>, v) w* F+ k+ \" f- C. E在第一点中对 socket 的创建步骤已有说明,首先需要完成 socket 的初始化操作,使用函数 WSAStartup,该函数的原型为: int WSAStartup(8 j. T" K1 u) {WORD wVersionRequired, LPWSADATA lpWSAData6 ]. n+ S v7 H* g! G4 }- s ); 该函数的参数 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)9 I! Z" p& J# HWSAStartup 若初始化失败则会返回非0值: if (WSAStartup(MAKEWORD(1, 1), &wsaData) != 0){9 l2 A+ @/ Z! q. `1 k* C exit(1);; [% A+ Y6 C5 a6 f: T ~( y( i } 2.2 创建socket 套接字 初始化完毕后开始创建套接字,套接字创建使用函数,函数原型为: SOCKET WSAAPI socket(- A1 _# U$ D% F- Lint af,1 Z6 R) s% Y6 ], W4 n int type, int protocol5 p% d' t7 T7 p, Y );8 p( A Z" v( \ U9 j 在函数原型中,af 表示 IP 地址类型,使用 PF_INET 表示 IPV4,type 表示使用哪种通信类型,例如 SOCK_STREAM 表示 TCP,protocol 表示传输协议,使用 0 会根据前 2 个参数使用默认值。 int skt = socket(PF_INET, SOCK_STREAM, 0); K7 g! v0 s7 ]( L创建完 socket 后,若为 -1 表示创建失败,进行判断如下: if (skt == -1){ return -1;0 r- E/ x8 T+ z' g" v% n# p } 2.3 绑定服务器 创建完 socket 后需要对服务器进行绑定,配置端口信息、IP 地址等。 首先查看 bind 函数需要哪一些参数,函数原型如下: int bind(; G/ S7 C- [* R6 c% r6 g4 lSOCKET socket,8 t+ P* a1 V) n' W/ Y! F, I5 S8 N$ _ const sockaddr *addr,8 f7 \/ e5 k6 I7 b$ p6 d5 _ int addrlen ); 参数 socket 表示绑定的 socket,传入 socket 即可;addr 为 sockaddr_in 的结构体变量的指针,在 sockaddr_in 结构体变量中配置一些服务器信息;addrlen 为 addr 的大小值。 通过 bind 函数原型得知了我们所需要的数据,接下来创建一个 sockaddr_in 结构体变量用于配置服务器信息: struct sockaddr_in server_addr;* @; c! E7 s9 r; J& l8 h随后配置地址家族为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;/ ]8 M& Z& b3 G8 h* }, F% g8 J6 T 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);/ V& l8 _. y/ M# R+ @2 @7 T 随后使用 bind 函数进行绑定且进行判断是否绑定成功: //绑定+ Y* B8 ?1 a! R" {/ |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;/ V* G$ S. W4 D; W) a* y" Z! l0 \ }4 w4 H/ s% N6 J 此阶段完整代码如下: #include <WinSock2.h>! ]% x7 Z0 A9 b#include<stdio.h> int main(){ //初始化 WSADATA wsaData;5 p% h) E6 a. ^0 m5 ` if (WSAStartup(MAKEWORD(1, 1), &wsaData) != 0) { exit(1);( }( F4 U: `7 x& z U; M% J }+ T& J/ D5 W+ z9 r* w+ X9 Q3 c //socket创建 c! ~1 b4 z) W8 P$ R) s4 k& [ int skt = socket(PF_INET, SOCK_STREAM, 0);8 d% ?1 [3 i/ D5 K7 U6 d7 s if (skt == -1) { return -1;" w$ Z( C- \: s: _4 B Y- A } //配置服务器 3 Y N7 [/ x4 E+ F! y/ k struct sockaddr_in server_addr; server_addr.sin_family = AF_INET;, W, t) v$ Z! R1 D3 o# V: r3 S/ S server_addr.sin_port = htons(8080); server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");, Q3 g. I- R2 c4 |1 ~. t8 W memset(&(server_addr.sin_zero), '\0', 8);* o+ r4 W: j( c$ @, W. f7 { //绑定 if (bind(skt, (struct sockaddr *)&server_addr,sizeof(server_addr)) == -1){ return -1; } 7 I* [- B6 i; j9 X3 h _ //监听 2 A' F( X+ U8 n- G; j* F( ?6 A+ r3 m if (listen(skt, 10) == -1 ) { ; ?& v& K1 m9 S' t+ c/ ?: a return -1; }# M/ Y: g9 b% E5 }4 [: ? % P4 F0 _9 a$ `( U5 L5 m7 |/ L printf("Listening ... ...\n"); }, k! t; `* `( @5 P1 g 运行代码可得知代码无错误,并且输出 listening:
2.5 获取请求 监听完成后开始获取请求。受限需要使用 accept 对套接字进行连接,accept 函数原型如下: int accept(int sockfd,! P* O' A! Z( h. u struct sockaddr *addr,% N o1 J4 u2 Y. r socklen_t *addrlen$ y W; r1 a5 S5 A% W: w ); 参数 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(" z- p1 C# ~- t. j: u1 bint sockfd, void *buf, 0 ^( g% f4 A8 p/ Q! U size_t len, int flags ) 参数 sockfd 为 accept 建立的通信;buf 为缓存,数据存放的位置;len 为缓存大小;flags 一般设置为0即可: //获取数据char buf[1024];6 o2 {1 f9 o% R5 X8 o3 l* r% A9 l if (recv(access_skt, buf, 1024, 0) == -1) { exit(1); }7 G* H" m; u* l% A+ l1 T Z- j 此时我们再到 accpt 和 recv 外层添加一个循环,使之流程可重复: while(1){//建立连接 ( r" w9 ]8 @; z+ _ printf("Listening ... ...\n");( h$ K, f7 |; ^2 A struct sockaddr_in c_skt; & l0 R. n" w- {0 ~" d2 Y' E: x 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); } } j7 g( T7 `% k: R4 r 并且可以在浏览器输入 127.0.0.1:8080 将会看到客户端打印了 listening 新建了链接:
我们添加printf语句可查看客户端请求: while(1){$ O$ C/ P6 E# \9 S1 m+ p//建立连接 & h$ k7 k* X( f- F! F printf("Listening ... ...\n"); struct sockaddr_in c_skt; ( Z, h; ]4 R! c. u$ ?9 U5 }2 o j5 @ int s_size=sizeof(struct sockaddr_in); int access_skt = accept(skt, (struct sockaddr *)&c_skt, &s_size);+ Z! E( Z3 Y; G) X# F //获取数据 - y4 U5 U6 \) r# P8 F. k" `: W char buf[1024]; if (recv(access_skt, buf, 1024, 0) == -1) {& v: ?; W/ Y& w2 E% m0 w0 [ exit(1); } . g/ T5 G9 E) r printf("%s",buf); }
接下来我们对请求头进行对应的操作。 2.6 请求处理层编写 得到请求后开始编写处理层。继续接着代码往下写没有层级,编写一个函数名为 req,该函数接收请求信息与一个建立好的连接为参数: void req(char* buf, int access_socket){! l" b& v p- V" [+ z! M A } 然后先在 while 循环中传递需要的值: req(buf, access_skt);接着开始编写 req 函数,首先在 req 函数中标记当前目录下: char arguments[BUFSIZ]; ) u7 A3 d' y/ |9 }7 {9 J/ wstrcpy(arguments, "./"); 随后分离出请求与参数: char command[BUFSIZ];sscanf(request, "%s%s", command, arguments+2);& ]# A0 E$ O+ K3 n9 w 接着我们标记一些头元素: char* extension = "text/html"; " U4 s0 n) o9 L2 schar* content_type = "text/plain"; char* body_length = "Content-Length: "; 接着获取请求参数,若获取 index.html,就获取当前路径下的该文件: FILE* rfile= fopen(arguments, "rb");9 B- k u8 [+ |, t0 k3 Q获取文件后表示请求 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) 5 }/ z6 c$ O' N8 }8 n{, \) k% G; s% m: d; `, S! f% H int total; int bytesleft; 5 f' \# z8 n. p; t0 X v9 \ int n; total=0; bytesleft=*len; while(total < *len) {: m d! ~0 x- r! {! Q5 D( @% [ n = send(s, buf+total, bytesleft, 0);, G0 T S' b" _7 I$ M( ] if (n == -1) 1 p& g* H5 B5 I# w0 }# _ {! i! r6 H& ?3 k break; } total += n; bytesleft -= n;: |' Q( i+ _ I% E4 j } *len = total; 8 d7 f2 Z, G# q+ p6 o& v2 R( ~ return n==-1?-1:0; 9 M G; ~$ l# q# \2 k } send 函数功能并不难在此不再赘述,就是一个遍历发送的逻辑。随后发送 http 响应与文件类型: send_(send_to, head, &len);len = strlen(ctype);. `. I; t" G* g4 o: o( K send_(send_to, ctype, &len); 随后获得请求文件的描述,需要添加头文件#include <sys/stat.h>使用fstat,且向已连接的通信发生必要的信息 : //获取文件描述struct stat statbuf;( f+ r: P% p: I5 e- c- t$ `) ^- ` char read_buf[1024]; 4 O, P+ {1 {: M! l4 ? 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);& S; ]- o* ^' X2 ?2 @5 R0 s& r2 d send(client_sock, "\n", 1, 0); send(client_sock, "\r\n", 2, 0); 最后发送数据: //·数据发送; n6 U3 w% ^% B7 J( F* Wchar read_buf[1024]; ( X, A. G, c' b0 [* M1 I0 b len = fread(read_buf ,1 , statbuf.st_size, rfile); if (send_(client_sock, read_buf, &len) == -1) { ' g$ h6 p8 ~& u8 f# u8 Q printf("error!"); , D0 K: i- f- Z' A }# Q; x( H7 C8 ]/ f; A, H 最后访问地址 http://127.0.0.1:8080/index.html,得到当前目录下 index.html 文件数据,并且在浏览器渲染:
所有代码如下: #include <WinSock2.h>#include<stdio.h> #include <sys/stat.h> $ g6 q! p; [( O9 Z! [' O int send_(int s, char *buf, int *len) {7 ~6 R. k" e: a int total; . `3 z& M/ D8 I+ y2 m int bytesleft; int n; total=0;0 p, Q! }0 Q1 I: D x+ m2 @ bytesleft=*len; while(total < *len) { n = send(s, buf+total, bytesleft, 0); if (n == -1) { break; } total += n; bytesleft -= n;2 a7 x' B" p2 Z Y- F- ~ }% V6 h6 W( C9 D3 Z2 c2 _ *len = total; return n==-1?-1:0; }, r4 X; r6 y+ b/ s/ ` void req(char* request, int client_sock) { char arguments[BUFSIZ]; 6 `: b5 Y7 _# s) i/ R# b* b strcpy(arguments, "./"); char command[BUFSIZ]; sscanf(request, "%s%s", command, arguments+2); char* extension = "text/html"; + x( P& W* a9 k. y1 ]* m7 m3 F9 ^ char* content_type = "text/plain"; # [: |4 `1 [/ P/ ~- d char* body_length = "Content-Length: "; FILE* rfile= fopen(arguments, "rb"); ( C% g6 v, Y& K$ b# t char* head = "HTTP/1.1 200 OK\r\n"; int len; ( v# v. F/ G) a2 C+ c3 b char ctype[30] = "Content-type:text/html\r\n"; 8 Q! ]. l/ r' w5 Z len = strlen(head); & Q" [ A& s1 n+ m& I1 A send_(client_sock, head, &len); len = strlen(ctype); send_(client_sock, ctype, &len);0 H" p* p) J& v* i. H q5 R% u* W 2 J7 X7 {* n, t: A/ M, _ 8 M. s2 a+ G* p struct stat statbuf;' {7 u! ~+ J+ Y 3 w: [3 U+ q! G: e+ Y9 O; V8 x8 i char length_buf[20];* m6 d% d* p5 v fstat(fileno(rfile), &statbuf);" \0 R5 o- _. @! L itoa( statbuf.st_size, length_buf, 10 );! b2 i G. w' x' e9 ?9 h send(client_sock, body_length, strlen(body_length), 0); {5 X* E/ q( R4 R- C send(client_sock, length_buf, strlen(length_buf), 0);0 {8 G) _% [! Z' I8 \ 0 z, {% D1 B+ o- r send(client_sock, "\n", 1, 0);" l4 x& y3 e i4 r" k' @ send(client_sock, "\r\n", 2, 0);, c4 o4 C' X2 I' S6 m 0 O2 S) ?+ T8 U3 X$ Y char read_buf[1024]; len = fread(read_buf ,1 , statbuf.st_size, rfile); if (send_(client_sock, read_buf, &len) == -1) { 4 c5 H L$ z2 t0 A1 O9 ~: _8 J printf("error!"); , L9 ~' D6 s5 ^# V; h } return; }( {# J/ {" A. R, j6 K ! Q6 c B- j3 G) l; L5 {6 k ) V# D+ t& ~, Q1 a) z' ^ int main(){% ]7 w9 ]! e4 { WSADATA wsaData;( V. J. ^/ h0 }( \% n* F/ } if (WSAStartup(MAKEWORD(1, 1), &wsaData) != 0) { exit(1);) u% H1 t4 R d1 e; u; i } int skt = socket(PF_INET, SOCK_STREAM, 0);; ?6 D% c' _, a# B9 H) K if (skt == -1) { 4 P% k }. d/ G: x0 B return -1;* V4 v5 k: q' f3 E: Y }8 {# a% }) i9 O0 x' `8 j0 A struct sockaddr_in server_addr; server_addr.sin_family = AF_INET;4 T/ Y! a0 x1 n server_addr.sin_port = htons(8080);* c; c9 t, B, B$ [ server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");: G* l b* d* P- D# m1 t( O memset(&(server_addr.sin_zero), '\0', 8); ! T) {1 p. R5 k- c if (bind(skt, (struct sockaddr *)&server_addr,sizeof(server_addr)) == -1) { ' [" ?3 z k6 X: y- H return -1; } 2 a& E4 b) C$ n# ?5 A if (listen(skt, 10) == -1 ) { return -1; } while(1){ printf("Listening ... ...\n");* Z j7 e" ~1 b! [) B3 \ struct sockaddr_in c_skt; 5 Z( U2 k0 |( y( ? Y' T: f U; [, c int s_size=sizeof(struct sockaddr_in); T, c4 f, x2 m- D( Y" d2 _1 P int access_skt = accept(skt, (struct sockaddr *)&c_skt, &s_size);2 w5 q/ J/ k1 a" ~ ; O; C$ N' ^. u5 w. k char buf[1024]; if (recv(access_skt, buf, 1024, 0) == -1) {8 g) H; v; ?$ `! E9 L exit(1); }* W( G% B0 }/ A" K, c3 ?( q: T/ [ req(buf, access_skt); } & v4 o0 d4 D4 h" r } 小伙伴们可以编写更加灵活的指定资源类型、错误处理等完善这个 demo。 |
微信公众号
手机版