C语⾔实现⼀个简单的web服务器
说到 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 表⽰
web服务器主要提供什么服务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);
}
接下来我们对请求头进⾏对应的操作。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论