|
说到 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( l, H8 `. ]) I. q+ JWORD 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) $ j9 A. l$ }( P* r- h{ exit(1); X# M9 U3 V' K% v* s }- c; h, N5 D$ v; t q: | 2.2 创建socket 套接字 初始化完毕后开始创建套接字,套接字创建使用函数,函数原型为: SOCKET WSAAPI socket(! q- o) n5 O } P% P- @int af,/ g# x, L# N3 k1 K$ ~7 C+ G, A int type, int protocol$ V: V0 i, F- {0 q0 ] ); 在函数原型中,af 表示 IP 地址类型,使用 PF_INET 表示 IPV4,type 表示使用哪种通信类型,例如 SOCK_STREAM 表示 TCP,protocol 表示传输协议,使用 0 会根据前 2 个参数使用默认值。 int skt = socket(PF_INET, SOCK_STREAM, 0);0 k. c! n/ ]+ I; G& Z创建完 socket 后,若为 -1 表示创建失败,进行判断如下: if (skt == -1){ return -1; } 2.3 绑定服务器 创建完 socket 后需要对服务器进行绑定,配置端口信息、IP 地址等。 首先查看 bind 函数需要哪一些参数,函数原型如下: int bind(. n' H2 i+ x2 ]" Y& p' rSOCKET socket,1 F7 j$ u6 M8 u, e& q0 I const sockaddr *addr, int addrlen );; c/ n, I/ o2 F) j# B- Q6 E* X S! R 参数 socket 表示绑定的 socket,传入 socket 即可;addr 为 sockaddr_in 的结构体变量的指针,在 sockaddr_in 结构体变量中配置一些服务器信息;addrlen 为 addr 的大小值。 通过 bind 函数原型得知了我们所需要的数据,接下来创建一个 sockaddr_in 结构体变量用于配置服务器信息: struct sockaddr_in server_addr;( ^! ^/ \0 N3 V3 \随后配置地址家族为AF_INET对应TCP/IP: server_addr.sin_family = AF_INET;! h4 ~! _) W( n/ ]1 N接着配置端口信息: server_addr.sin_port = htons(8080);再指定 ip 地址: server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");ip 地址若不确定可以手动输入,最后使用神器 memset 初始化内存,完整代码如下: //配置服务器 ! V2 T; e5 ?6 _# ~) ]struct sockaddr_in server_addr; server_addr.sin_family = AF_INET;' u2 G) R4 H, J" ]& w2 a$ H server_addr.sin_port = htons(8080); server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");5 U& n5 B( I$ A9 u' t! G% e memset(&(server_addr.sin_zero), '\0', 8); 随后使用 bind 函数进行绑定且进行判断是否绑定成功: //绑定+ y# z0 ]% i, A, X$ q2 Xif (bind(skt, (struct sockaddr *)&server_addr,sizeof(server_addr)) == -1) { return -1; } 2.4 listen进行监听 绑定成功后开始对端口进行监听。查看 listen 函数原型: int listen(9 L6 g2 m4 }! {) A, S& yint sockfd, int backlog, b$ Z _2 d' `2 E )6 |$ ~) ^5 D5 P& b [, Z4 D 函数原型中,参数 sockfd 表示监听的套接字,backlog 为设置内核中的某一些处理(此处不进行深入讲解),直接设置成 10 即可,最大上限为 128。使用监听并且判断是否成功代码为: if (listen(skt, 10) == -1 ) { ( X# a- b4 K! L- a" Z$ l4 @7 R3 Preturn -1;0 H" ?0 T" }9 D5 }- A d }: D! L5 O8 u6 `1 n 此阶段完整代码如下: #include <WinSock2.h>#include<stdio.h> int main(){ //初始化 WSADATA wsaData;" O8 h9 `+ S( M, J4 M1 e! K Z if (WSAStartup(MAKEWORD(1, 1), &wsaData) != 0) { exit(1);& m. e% W8 Z3 S5 d o* H } //socket创建 ' S& w; u/ G! d int skt = socket(PF_INET, SOCK_STREAM, 0);/ I$ h8 _5 `! F' P& x8 r, n' a, K if (skt == -1) { return -1; } //配置服务器 $ Z2 p1 [! T+ l& v struct sockaddr_in server_addr;5 B5 f5 C% C; l server_addr.sin_family = AF_INET; server_addr.sin_port = htons(8080);* f6 z) p! q( F! Q G+ E server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");0 H/ s' Y/ ~- r memset(&(server_addr.sin_zero), '\0', 8); //绑定 if (bind(skt, (struct sockaddr *)&server_addr,sizeof(server_addr)) == -1){ return -1; " ~! Y! A; r, [ } //监听 if (listen(skt, 10) == -1 ) { return -1;# p$ B8 P- m9 V6 G6 K, j3 p$ w } printf("Listening ... ...\n"); }6 ]9 y1 K) ]8 i) G 运行代码可得知代码无错误,并且输出 listening:
2.5 获取请求 监听完成后开始获取请求。受限需要使用 accept 对套接字进行连接,accept 函数原型如下: int accept(int sockfd,5 Z* }, P' J3 R3 C9 v struct sockaddr *addr, socklen_t *addrlen& o% ]0 M3 i' k- h8 r7 G ); 参数 sockfd 为指定的套接字;addr 为指向 struct sockaddr 的指针,一般为客户端地址;addrlen 一般设置为设置为 sizeof(struct sockaddr_in) 即可。代码为: struct sockaddr_in c_skt; 0 C1 f+ ~) y9 e _; s2 bint s_size=sizeof(struct sockaddr_in);; m$ q. b* g% h% R5 p. O' f* _) { int access_skt = accept(skt, (struct sockaddr *)&c_skt, &s_size);6 t2 j# y) \$ ~$ V+ f1 j9 C 接下来开始接受客户端的请求,使用recv函数,函数原型为: ssize_t recv(; g) r4 i# a: A8 _4 r qint sockfd, void *buf, 2 P, u# d7 j- s1 m: j H size_t len, " @1 e, C& {$ ^4 n int flags ) 参数 sockfd 为 accept 建立的通信;buf 为缓存,数据存放的位置;len 为缓存大小;flags 一般设置为0即可: //获取数据char buf[1024];8 g0 U+ ?% }& ~1 b4 P if (recv(access_skt, buf, 1024, 0) == -1) {; k- G4 `6 K) v0 A' i exit(1); } 此时我们再到 accpt 和 recv 外层添加一个循环,使之流程可重复: while(1){//建立连接 ?# N. K) C) |, `) n# C, b$ | printf("Listening ... ...\n");. T# I8 K0 p/ G5 S struct sockaddr_in c_skt; int s_size=sizeof(struct sockaddr_in); int access_skt = accept(skt, (struct sockaddr *)&c_skt, &s_size);3 \3 W' ?$ Y( ]. z, N# _2 ?: @ 5 e& c5 Z, t& K4 D- `' _: j! Q //获取数据 % M B: d2 C" F; h j& l char buf[1024];0 d9 r) J$ z' W* b: y4 k if (recv(access_skt, buf, 1024, 0) == -1) { exit(1); }& X! T7 g" B1 U/ u H } $ }0 S, _# H3 T3 g M 并且可以在浏览器输入 127.0.0.1:8080 将会看到客户端打印了 listening 新建了链接:
9 ]' [0 o& M: w. | 我们添加printf语句可查看客户端请求: while(1){' I. R& d- d' F6 R. C//建立连接 9 d( y# _- R: M5 I) U) r printf("Listening ... ...\n"); struct sockaddr_in c_skt; int s_size=sizeof(struct sockaddr_in);. m. X2 q! Y. x$ n; \" z+ m7 h8 ]6 s int access_skt = accept(skt, (struct sockaddr *)&c_skt, &s_size); ; d( H9 L- {" X; a //获取数据 - G* ^$ z* o- @1 C char buf[1024];1 B2 {; } A0 W0 n. a if (recv(access_skt, buf, 1024, 0) == -1) { exit(1);- F, v# ^( b7 ]6 [5 C+ J2 {0 d6 r }! `' ~) I( \+ v( W' o0 y8 U 6 e8 r9 \0 `+ i& c7 @ printf("%s",buf);- h1 A" I% D, h- L( i& t } 5 A1 H$ a/ n4 s7 c% u# r# r* p9 S6 p
接下来我们对请求头进行对应的操作。 2.6 请求处理层编写 得到请求后开始编写处理层。继续接着代码往下写没有层级,编写一个函数名为 req,该函数接收请求信息与一个建立好的连接为参数: void req(char* buf, int access_socket){ }5 D* H9 U$ P, o( q, J 然后先在 while 循环中传递需要的值: req(buf, access_skt);+ F, U8 {. L: t) Z接着开始编写 req 函数,首先在 req 函数中标记当前目录下: char arguments[BUFSIZ]; 6 z) }2 {8 l; [strcpy(arguments, "./"); 随后分离出请求与参数: char command[BUFSIZ]; ( C; E, ` {2 P, I4 B) D ksscanf(request, "%s%s", command, arguments+2); 接着我们标记一些头元素: char* extension = "text/html"; 6 I0 {% q- G) h+ _' c" {/ A9 Y0 Nchar* content_type = "text/plain"; char* body_length = "Content-Length: ";: X- j" u' U0 D. [* s2 ?- p( M 接着获取请求参数,若获取 index.html,就获取当前路径下的该文件: FILE* rfile= fopen(arguments, "rb");) z Y1 c4 L- z ]" e获取文件后表示请求 ok,我们先返回一个 200 状态: char* head = "HTTP/1.1 200 OK\r\n"; 4 B( o! c* {. |) ?+ N0 @int len; char ctype[30] = "Content-type:text/html\r\n"; len = strlen(head); 接着编写一个发送函数 send_: int send_(int s, char *buf, int *len) 3 z+ T& G& {- g8 B% q0 l{7 [1 {: l; K" L# w# d' B' i int total; int bytesleft; int n;0 _2 c& l4 |2 Y8 ?# ^- M: _ total=0;( ]9 ~6 u7 a% U) v! N f bytesleft=*len;" ]8 Z1 c5 i! w' S" j1 t0 d2 s" } while(total < *len) 2 ?' A* u1 l6 c" V7 s0 P( q {4 v7 f, c Z* z) V, Q; _ n = send(s, buf+total, bytesleft, 0); if (n == -1) { break; }" t: i8 q8 T0 S6 W5 z" l$ c total += n; bytesleft -= n; }3 B( x" c+ M/ u, R/ D9 t. W *len = total; / I& A9 I% @# i, D+ v3 K return n==-1?-1:0; . L* i+ o% B. A7 J3 C0 D, P }! Y. t3 f! T! r+ w7 J8 a+ ^ 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];( C; ~) z# {8 i6 I4 w2 t fstat(fileno(rfile), &statbuf); itoa( statbuf.st_size, length_buf, 10 );. F# x* }! S8 m4 S! r send(client_sock, body_length, strlen(body_length), 0);9 Y1 u: s- o. F1 V9 q5 C send(client_sock, length_buf, strlen(length_buf), 0);2 R: r2 U w- W; v 5 H* P% F2 k% d" G send(client_sock, "\n", 1, 0); send(client_sock, "\r\n", 2, 0);4 x4 d, Q; g9 B0 z/ G$ M6 a 最后发送数据: //·数据发送$ L. N$ T7 H! C! ?8 {# ochar read_buf[1024]; 5 y3 B. c7 ~' C7 B2 B7 A( X len = fread(read_buf ,1 , statbuf.st_size, rfile); if (send_(client_sock, read_buf, &len) == -1) { printf("error!"); }. P/ ~$ h2 a/ j3 Y e1 E$ P 最后访问地址 http://127.0.0.1:8080/index.html,得到当前目录下 index.html 文件数据,并且在浏览器渲染:
所有代码如下: #include <WinSock2.h> F3 |) G/ h; ]% n4 L. D3 p#include<stdio.h> #include <sys/stat.h> , N0 E8 _9 l: t 1 j3 p; v* k% ^1 M int send_(int s, char *buf, int *len) {0 u! v, i' `( B' Z, g int total; 3 R9 R) F/ ^7 _- q int bytesleft; int n;6 u) r$ x* V3 f# G% o; w2 E; u" q total=0;5 A$ ]1 `' ]4 K2 a" |9 y" o bytesleft=*len;5 w, t$ O3 `9 ~1 e, U& J1 I while(total < *len) $ f7 _5 ?6 I* Y& i% q" d2 J {$ V- g, S" _& t5 I# x n = send(s, buf+total, bytesleft, 0); if (n == -1) ) m' k# d4 h3 H, e5 O { break;3 i; ^2 b( o$ d- k: ~ } total += n;5 ?/ p) F9 e* i2 @ V( _ bytesleft -= n; }! z2 Q3 T' [' d) s7 B *len = total; return n==-1?-1:0; 7 y o( Y# J: u } ( G) Y0 R7 i7 F void req(char* request, int client_sock) { 4 J) c: y4 U- W: P5 B char arguments[BUFSIZ]; : F* W6 a1 p* r9 m strcpy(arguments, "./");4 d/ x s9 O% C0 E+ w3 I1 r# f char command[BUFSIZ]; ) g8 C6 E P6 M) H Y 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"); ' F, ~# i% @1 d3 K- z& H 8 X' ?" \" J+ [2 u) m x6 k char* head = "HTTP/1.1 200 OK\r\n"; int len; char ctype[30] = "Content-type:text/html\r\n"; : K! t$ M- l+ _1 q; } len = strlen(head); G/ y; l+ G( D: X" S ) l5 @1 U r7 A1 I* V# j( L3 K1 G0 g send_(client_sock, head, &len);: M Z9 t' q9 Q5 }0 q8 } len = strlen(ctype);- {0 ?, T1 H/ h% z: G) R send_(client_sock, ctype, &len);* Z- O+ ~5 s9 S. Q0 M* M1 i5 G1 ?9 X 5 d2 ~. o$ S$ C Q- O! m7 }/ b8 d7 r, S struct stat statbuf;; Q' M9 e0 A8 v% D% O / p/ d+ t7 e8 ` char length_buf[20]; fstat(fileno(rfile), &statbuf); itoa( statbuf.st_size, length_buf, 10 );; W, D) s' o/ l1 w) A. B/ y* S) d+ L send(client_sock, body_length, strlen(body_length), 0);: q2 Y( U6 @& D# f6 x: [% | send(client_sock, length_buf, strlen(length_buf), 0);' E4 y2 |. F! h9 n$ u7 }; x 3 N- U- O9 { m; I" c, l* I2 O9 S# @ send(client_sock, "\n", 1, 0); send(client_sock, "\r\n", 2, 0); char read_buf[1024]; . r/ ?) [# f8 V3 J2 R len = fread(read_buf ,1 , statbuf.st_size, rfile);! O0 f% Y- y+ r' [% j: b$ a! p$ F& G if (send_(client_sock, read_buf, &len) == -1) { printf("error!"); / Z8 G+ [' @2 w# b9 j }& E( }' E$ p3 S6 O return;; d: b$ ]4 a5 r }. a$ i0 ?* `# e4 j 2 b5 z; ~% X# F( y+ h6 _( B1 n4 [ v& K, J, U; P, k4 }5 x int main(){) z; D9 z* B4 }- A" g- f/ m6 X0 j! Z WSADATA wsaData; if (WSAStartup(MAKEWORD(1, 1), &wsaData) != 0) { exit(1); } $ i" n. a l5 h# C# ] int skt = socket(PF_INET, SOCK_STREAM, 0); if (skt == -1) { * T2 v: P9 S# X# X return -1;7 n2 ~5 t5 N5 J7 v1 K } struct sockaddr_in server_addr; w/ O2 @* X$ p+ } server_addr.sin_family = AF_INET; server_addr.sin_port = htons(8080);& C6 E5 B. j3 B: @; x, y/ | server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); memset(&(server_addr.sin_zero), '\0', 8);! O9 q/ w& ?/ v5 V @4 n 8 b& g! o1 a2 _) x6 |+ H. @) z if (bind(skt, (struct sockaddr *)&server_addr,sizeof(server_addr)) == -1) { return -1; , v/ D, @: ?$ Z s. R } if (listen(skt, 10) == -1 ) { return -1; } while(1){ / X Z/ S; G" m0 f: w0 ~( C printf("Listening ... ...\n");5 O$ `8 l* O: M# T0 v7 _6 n6 ` struct sockaddr_in c_skt; 8 K) f- v& { ]5 f, D int s_size=sizeof(struct sockaddr_in);6 V; j( F$ M/ t7 I$ y int access_skt = accept(skt, (struct sockaddr *)&c_skt, &s_size); char buf[1024]; if (recv(access_skt, buf, 1024, 0) == -1) { exit(1); }8 j! Z: p0 v& v* C5 A- t+ G" f " S1 i+ H5 o. y4 Y* s req(buf, access_skt);$ T# B7 @ N, d8 e, `( h } } 小伙伴们可以编写更加灵活的指定资源类型、错误处理等完善这个 demo。 . d% X# D! g: Y& t' E0 C2 ` |
微信公众号
手机版