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。

相关文章
相关标签/搜索