共计 7474 个字符,预计需要花费 19 分钟才能阅读完成。
说到 web 服务器想必大多数人首先想到的协定是 http,那么 http 之下则是 tcp,本篇文章将通过 tcp 来实现一个简略的 web 服务器。
起源:公众号(c 语言与 cpp 编程)
本篇文章将着重解说如何实现,对于 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 示意 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。