说到 web 服务器想必大多数人首先想到的协议是 http,那么 http 之下则是 tcp,本篇文章将通过 tcp 来实现一个简单的 web 服务器。
本篇文章将着重讲解如何实现,对于 http 与 tcp 的概念本篇将不过多讲解。
既然是基于 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.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 OK\r\n";
int len;
char ctype[30] = "Content-type:text/html\r\n";
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, "\r\n", 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>
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 OK\r\n";
int len;
char ctype[30] = "Content-type:text/html\r\n";
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, "\r\n", 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。
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/kMpFFjY2jeEERQLYmvUp3g
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。