11
古风

C 语言实现一个简单的 web 服务器

作者:古风 本文共2张图片 8498个字,阅读需要22分钟 百度已收录

到 web 服务器想必大多数人首先想到的协议是 http,那么 http 之下则是 tcp,本篇文章将通过 tcp 来实现一个简单的 web 服务器。

本篇# { w F F 4 D K文章Y C b _将着重讲解如何实现,对于 http 与 tcp 的概念本篇将不过多讲解。p p x u 6 Q

一、了解 Socket 及 web 服务工作原理

既然6 z v 6是基于 tcp 实现 web 服务器,很多学习 C 语言的小伙伴可能会很快的想到套接字 socket。socket 是一个较为* J L @ : E抽象的通信进程,或者说是主机与主机进行信息交互的一种抽象。socket 可以将数据流送入网络中,也可以接收数据流。

socket 的信Q c t Q :息交互与本地文件信息的读取从表面特征上看类似,但其中所存在的编写复杂度是本地 IO 不能比拟的,但却有相似点。在 win 下 soE A P . C Lcket 的交互交互步骤为:WSAStartup 进行初始化--> socket 创建套接字--> bind 绑定-D ) i X I 4 3-> listen 监听` M * [ o m , e s--> connect 连接--> accepI @ ? B / 9t 接收请求--> sen; ~ 2 T r Fd/recv 发送或接收数据--> closesocket 关闭 soc- p ^ 7 9ket--> WSACleanup 最终关闭。

C 语言实现一个简单的 web 服务器-IT博客园

了解完了一个 socket 的基本步骤后我们了解一下一个基本 web 请求的用户常规操作,操作分为:打开浏览器-->输入资源地址 ip 地址-->得到资源。当目标服务器接收到该操作产生掉请求后,我们可以把服务器的响应流程步骤看为:获得 request 请求-->得到请求关键数据-->获取关键数据-->发送关键数据。服务器的这一步流程是在启动socket 进行监听后才能响应。通过监听得知接收到请求,使用 recv 接收请求数据,从而根据该参数得到进行资源获取,最后通过 seW & i nd 将数据进行L 3 r Z H B E返回。

二、创建sokect完9 ) F D I b成监听

2X n D a 2.1 WSS v 1 M 4 : 0 IAStartup初始化

首先在c语言头文件中引入依赖 WinSock2.h:

#include<WinSock2.h>

在第一点中对 sock, i 5 R ( }et 的创建步骤已有说明,首先需要完成 socket 的初始化操作,使用函数 WSAStartup,该函数的原型为:

intWSAStartup(
WORDwVe( u M ,rsionRequired,
LPWSADATAlpWSAData
);

该函数的参数 wVersionRequired 表示 WinSock2 的版本号;lpWSAData 参数为指向 WSADATA 的指针,WSADATA 结构用于 WSAStartup 初始化后返回的信息。

wVersi^ & l 8 [ T } L NonRequired 可以使用 MAKEWORD 生成,在这里可以使z Y w =用版本 1.1 或版本2.2U b p F H 7,1.1 只支持 TCP/IP,h 0 + b K 5 x x 6版本 2.1 则会有更多H H X O 6 8的支持,在此我们选择版本 1.1。

首先声明一个 WSADATA 结构体 :

WSADATAwsaData;

随后传参至初始化函数 WSAStartup 完成初始化:O , u A c S

WSAStartup(MAKEWORD(1,1),&wsaData)

WSAStartup 若初始化失败则会返回非0值:

if(WSAStartup(M, / 5 U K j J &AKEWORD(1,1),&wsaData)!=0)
{
exit(1);
}

2.2 创建socket 套接字

初始化完毕后开始创建套接字,套接字创建使用函数,函数原型为:

SOCKETWSAAPIsocket(
intaf,
inx g ~ ^ k V Q Qttype,
intprotocol
);

在函数原型中,af 表示 IP 地址类型,使用 PF_I# R 3 V 7 N xNET 表E h 5 ) [ h 9示 IPV4,type 表示使用哪种通信类型,例如 SOCK_STREAM 表示 TCP,protocol 表示传输协议,使用 0 会根据前 2 个参数使用默认值。

intskt=socket A 8(PF_INET,SOCK_u 7 Q L ) JSTREAM,0);

创建完 s! x { ! G W { x nocket 后,若为 -1 表示创建失败,进行判断如下:

if(skt=H g u $ t D # &=-1)
{
return-1;
}

2.3 绑定服务器

创建完 socket 后需要对服务器进m z W行绑定S r * ; e } s,配置端口信息、IP 地址等。首先查看 bind 函数需要哪一些参i V i F _ V 9 数,函数原型如下:

intbind(
SOCKETso  ? T C o , &ocket,
constsockaddr*addr,
intaddrlen
);

参数 socket 表示绑E ( M j定的 socket,传入 socket 即可;addrq u K N @ / r 为 sockaddr_in 的结构体变w 4 # P A : K ` R量的指针,在 sockaddr_in 结构体变量中配置一些服务器信息;aD d e x s ) 3ddrlen 为 addr 的大小值。

通过 bind 函数原型得知了我们所需要的数据,接下来创h o *建一个 sockaddr_in 结构体变U 4 f 9 e量用于配置服务器信息:

structsockaddr_inserver_addr;

v l w后配置地址家族为AF: g L $_INET对应TCP/IP:

server_addr.sin_family=AF_INET;

接着配置端口信息:

server_addr.sine z ^ ! n_port=htons(8080);

再指定 ip 地址:

server_addr.sin_addr.s_addr=8 W } kinet_addr("j q 0127.0.0.1");

ip 地址若不确定可以手动输入,最后使用神器 memset 初始化内存,完整代码如t 0 n下:

//配置服务器
structsockaddr_inserver_addr;
server_addr.sin_family=AF_INEg 5 O VT;
server* b k_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,(structsockaddr*)&server_addr,sizeof(server_addr))n r W d 1==-1){
return-1;
}

2.4 listen进行监听

| F L定成功后开始对端口进行监听。查看 listen 函数原型:

intlistenC 3 d ~(
intsoc, C , w d J Xkfd,
intbacklog
)

函数原型中,参数 sockfd 表示监听的套接字,backlo) q 4 #g 为设置内核中的某一些处理(此处不进行深入讲解),直接设置成 10 即可,最大上限为 128。使用监听并且判断2 A ; q ~ v b 是否成功代码为:

if(listen(skt,10)==-1){
return-1;
}

此阶段完整代码如下:

#( t |include<WinSock2.h>
#include<stdio.h>
intmain(){
//初始化
WSADATAwsaData;
if(WSAStartup(! : /MAKEWORD(1,1),&wsaData)!=0){
exit(1);
}
/ @ 2 | G C/socket创建
inta @ v _skt=socket(PF_Ii 8 WNET,SOCK_STREAM,7 X 8 N0);
if(skt==-1){
return-1U Y Q q A u ~ V;
}
//配置服务器
structsockaddr_inserver_addr;
sel S ` 7 4 H L j prver_addr.sin_family=AF_A K J aINET;
server_addr.sin_port=htons(8080)x V V |  X;
server_addr.sin_addr.s_addr=inet_addr("127.0.0.1");
memset(&(server_addr.sin_zero),'\0',8);
//绑定
if(bind(skt,(structsockadK 8 ,dr*)&server_addr,sizeof(server_addr))==-1){
return-1;
}
/@ K B ( J l m/监E ( L I听
if(listen(skt,10)==-1){
return-1;
}

printf("Listening......\n");
}

运行代码可得知代码无错误,@ 5 Y _ O7 x s t A R $ -且输出 listening:

在这里插入图片描述

2.5 获取请求

监听完成后开始获取请求。受限需要使用 accept 对套接字进行连接,accept 函数原型C * H z d 0如下:

intaccept(
intsockfd,
sth k 1 Lructsockaddr*addr,
socklen_t*addrlen
);

参数 sockfd 为指定的套接字;addr 为指向 struct sockaddr 的指S ] . Z b 3 ( n 0针,一般为客户端地址;addrlen 一般设置为设置为 sizeof(struc_ . Pt sockac n ) Rddr_in) 即可。代码为:

structsockad^ m ) ! x 7  ~dr_inc_skt;
ints_size=sC z c _ , fizeof(structsockaddr_in);
intaccess_skt=accept(skt,(structsockaddr*)&c_skt,&s_si _ a n zize);

接下来开始接受客户端的请求,使用recv函数,函数原型为:

ssize_trecv(
intsockfd,
void*buf,
size_tlen,
intflags
)

参数 sockfd 为 accept 建立的通信;bu@ U :f 为缓存,数据存放的位置;len 为缓存大小;flags 一般设置为0即可:

//获取数据
charbuf[1024];
if# Q 3(recv(access_skt,buf,102Q H 9 W4,0)==-1){
exF 1 $ ` r I Nit(1);
}

此时我们再到 accpt 和 recv 外层添加一个循环,使之流程可重复:

while(1){
//建立连接
printf("Listening......\n");
structsockaddr_inc_skt;
ints_sizeV ) )=sizeof(structsockaddr_in);
intaccess_skt=accept(skt,(structsockaddr*)&c_sK J ] A O hkt,&s_size);

//获取数据
charbuf[102l ( -4];
if(recvM [ X : F(access_skt,buf,1024,0)==-1){
exit(1);
}
}

并且可以在浏览器输入 127.0.0.1:8080 将会看到客户端打印了 listening 新建了链接:

[gaL e E o ^llm s | # i 0 6 ]ery columns="1" size="full" ids="9236"]

我们添加printf语句可查看客户端请求:

while(6 G ?1l ; f x e +  B){
//建立连接
printf("Listening......\n");
structsockaddr_inc_skt;
ints_size=sizeof(structsockaddr_in);
intaccess_skt=accept(skt,(structsockaddr*)&c_skt,&s_size);

//获取数据
charbuf[1024];
it d H d E yf(recv(access_skt,buf,1024,0)==-1){
exit(1);
}

printf("%s) % x",buf);
}

接下来我们对请求头进行对应的操作。

2.6 请求处理层编写

得到请求后开始编写处理层。继续接着代码往下写没有层级,编写一个函数名为] N d n Z req,该E 1 w + A 1 % 9 |函数接收请求信息与一个建立好的连接为参数:

voidreq(char*buf,intaccess_socket)
{
}

然后先在 while 循环中传递需2 7 Z _ 8 0要的值:

req(buf,access_skt);

接着开始编写 req 函数,首先在 req 函数中标记当前目录下:

chararguments[BUFSIZ];
strcpy(arguments,"./");

随后分离出请求与参数:

charcommand[BUFSIZ];1 p ( +  G s
sscanf(requesj .  a m Wt,"%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 状l & h U f a态:

char*head="HTTP/1.1200OK\r\n";
intlen;
charctype[30]="Content-type:text/html\r\n";
len=strlen(head);

接着编写一个发送函数 send_:

intsend_(inu V m L ,  UtL v - b a k S P hs,char*buf,id I Q # { A . snt*len)
{
inttotal;
intbytesleft;
intn;
total=0;
bytesleft=*len;
while(q { d c y ] X yto Q w ) . Z M Rtal<*len)
{
n=send(s,buf+total,bytesleft,0);
if(n==-1)
{
break!  };
}
total+=n;
bytesleft-=n;
}d % t i N L J a
*len=total;
returnn==-1?-1:0;
}

send 函数功能并不难在此不再赘述,就是一个遍历发送的逻辑。随后发送 http 响应与文件类型:

send_(send_to,head,&len);
len=strlen(ctype);
send_(send_to,ctype,&len);

随后获得请求文件的g 9 Y =描述,需要添加头文件#include <sys/stat.h>使用fstat,且向Q K !已连接的通信发生必要的信息 :

//获取文件描述
stf h Rructstatstatbuf;
charread_buf[1024];
charlength_buf[20];
fstat(fileno(r? ] 5file),&statby e . 7 , ^ E }uf);
itoa(statbuf.st_size,length_buf,10);
send(client_s{ R 2 # ) 8 ~ aock,body_length,strlen(body# g A N H C ^ q C_lengt6 U # Q [ A ^h),0);7 ~ = = v T o  B
send(client_sock,length_buf,1 Y % wstrlen(length_buf),0);

send(client_sock{ 0 c | 1,"\n",1,0);
send(client_sock,"\r\n",2,0);

最后发送数据:

//数据发送
charread_buf[1024` n v 1];
len=fread(read_buf,1,statbuf.st_size,rfile);
if(send_(cli3 ? c Lent_sock,read_buf,&len)==-1){
printf("error!"j F l W $);
}

最后访问地x ~ h V 5 p l址 http://127.0.0.1:8080/index.html,得到当前目录下 index.html 文件数据,并且在浏览器渲染:

C 语言实现一个简单的 web 服务器-IT博客园

f s v ~ E _有代码如下:

#include<WinSo5 X B 0ck2.h&p V d S k / 6 r Igt;
#include<stdio.h>
#include<sys/stat.h>% { r

intsend_(ints,char*buf,int*lenX E 6 { !){
intta W X |otal;
int, + h W Jbytesleft;
intn;
total=0;
bytesleft=*len;
while(total<*lf T X Y K _ 4en)
{
n=send(s,buf+total,bytesleft,0);
if(n==-1)
{
break;
}
totaY u s ;l+=n;
bytesleft-=n;
}
*len=total;
returnn==-1?-1:0;
}

voidreq(char*request,intcf ~ 0lient_sock){
chararguments[BUFSIZ];
strcpy(argume u A U q v V ynts,"./");

charcommand[BUFSIZ];
sscanf(request,"%s%s",command,argument/ ; Gs) k J M O : m Y+5 t _ l2);

cha5 y o s , 5 ; z Wr*extension="teb % _  3 Vxt/html";
char*content_type="text/plain";
char*body_length="Content-Length:";

FILE*rfile=fopen(arguments,"rb");


char*head="HTTP/1.1200OK\r\n";
intlen;
charctype[30]="Content-j 6 (type:text/html\r\n";
len=strlen(head);

seJ / 5 ! & 9nd_(cl) - ` -ient_sock,head,&am% 3 ; n . M o (p;len);
len=strlen(ctype);
send_(client_sock,ctype,&len);


structstatstatbuf;

charlength_buf[20];
fstat(fileno(rfile),&statbuf);
itoa(statbuf.st_size,len l H cngth_buf,10);
send(client_sock,body_length,C & @ Y 6 estrle] N / d  h W Kn(body_length),0);
send(client_L ; v Y : p  =sock,length_buf,strlen(length_buf),0) F v R * o /;

send(client_sock,"\n",1,0);
send(client_sock,"\r\n",z : K2,0);


charread_buf[1024];
len=fread(read_buf,1,statbuf.st_size,rfile);
if(send_(@  - E ; x o lcliK } M & D u tent_sock,read_buf,&len)==-1){
printf("error!");
}

return;
}


intmain(){
WSADATAwsaDat% g ` D la;
if(WSAStartup(MAKEWORD(1,1),&w4 # ?saData)!=0){
exit(1);
}

intskt=socket(PF_INET,SOCK_STREAM,0);
if(skt==-1){
returnU , H v : a D-1;
}

structsockaddr_inserver_addr;
server_addr.sin_family=AF_INET;
server_addr.sin_po^ ! ? z 7 Y p & ]rt=htons(8080);
server_addr.sin_addr.s_2 r 8 0addr=inet_addr("1l v  H27.0.0.1");
memset(&(server_addr.sin` ^ k p 6_zero),'\0',8);

if(bind(skt,(structsockaddr*)&server_addr,sizeof(server_addr))==-1){
return-1;
}

if(listen(skt,10)==-1){
return-1;
}

while(1){

pr9 p Y * [intf(1 q  O {"Listening......\n");
structsockaddr_inw B L hc_skt;
ints_size=sizeof(structsockaddr_in);
intK m M B / i = Taccess_skt=accept8 C h g 5 l .(skt,(structsockadw & ` | } 4dr*)&c_skt,&s_size);

charbuf[1024];
if(recv(access_skt,buf,1024,0)==-1){
exit(1);
}

req(buf,access_skt);
}

}[ { ? K 0

小伙伴们可以Z 7 + k B编写更加灵活的指定资源类型、错误处理等完善这个 dC } j demo。


关注微信公众号『 淘游宝 』

第一时间了解最新网络动态关注站长不迷路~

常见问题FAQ

免费下载或者VIP会员专享资源能否直接商用?
本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。

Leave a Reply


赞助 VIP 海量资源随便下
来不及找到心仪的内容?按Ctrl+D收藏我们 发现更多
仅开放社交账号登录