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

news2024/11/16 1:17:50

说到 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 表示 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。

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/642681.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

高并发下数据一致性问题:数据库+缓存双写模式分析

前言 随着互联网业务的发展&#xff0c;其中越来越多场景使用了缓存来提升服务质量。从系统角度而言&#xff0c;缓存的主要目标是减轻数据库压力&#xff08;特别是读取压力&#xff09;并提高服务响应速度。引入缓存就不可避免会涉及到缓存与业务数据库数据一致性的问题&…

充电桩计量检测TK4860E交流充电桩检定装置

固定检定点&#xff1a;系统预设24A220V和12A220V的检定点&#xff0c;可完成单相32A或单相16A充电桩的工作误差的检定&#xff0c;24A和12A检定点的设计既可满足实际输出达不到额定电流的单相充电桩的检定&#xff0c;又可保证检定效率。 固定电量&#xff1a;系统可预设充电…

源代码安全管理

现在&#xff0c;随着软件开发公司对源代码保护的日益重视&#xff0c;源代码已成为企业核心竞争力的关键因素之一。为了确保企业在同行中展露头角并具备核心竞争力&#xff0c;源码的保护变得至关重要。 目前&#xff0c;源代码加密有两种常用方式&#xff1a;物理性和软件性…

leetcode 139.单词拆分

题目描述 给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。 注意&#xff1a;不要求字典中出现的单词全部都使用&#xff0c;并且字典中的单词可以重复使用。 来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 链…

ASEMI代理NXP快恢复功率二极管BYC30W-600P参数

编辑-Z BYC30W-600P参数描述&#xff1a; 型号&#xff1a;BYC30W-600P 重复峰值反向电压VRRM&#xff1a;600V 峰值工状向电压VRWM&#xff1a;600V 反向电压VR&#xff1a;600V 平均正向电流IF&#xff1a;30A 正向电压VF&#xff1a;1.38V 反向恢复时间trr&#xff…

iptables trace使用

iptables规则链流向&#xff1a; modprobe ipt_LOG ip6t_LOG nfnetlink_log加载内核模块 写入iptables规则&#xff0c;-A XXX -j TRACE iptables --version查看iptables版本&#xff0c;nf_tables则使用xtables-monitor --trace监听iptables规则流动。legacy则使用/var/log…

也谈现在网站行业是否还有必要做下去?

我是卢松松&#xff0c;点点上面的头像&#xff0c;欢迎关注我哦&#xff01; 想就这个话题唠叨几句的起因&#xff0c;是前几天拜读了卢松松发表的一篇题为**《为什么说网站行业不能做了?》**的文章。文章内容是卢松松对网站行业的现状随意的吐槽&#xff0c;没想到有位网友…

第十五个“世界海洋日”:水声功率放大器能为海洋水下声呐研究做些什么?

2023年6月8日&#xff0c;第十五个“世界海洋日”到来&#xff0c;今年的海洋日我们除了要聚焦海洋生态保护和海洋资源的可持续发展及利用&#xff0c;我们同样把视线聚焦在海洋科学研究上&#xff0c;海洋水下声呐技术&#xff0c;就是我们本次的主题。 作为能良好驱动声呐&am…

线性回归算法(含示例代码)

1 知识点讲解 1.1 线性回归 线性回归是一种常见的机器学习算法&#xff0c;用于预测连续型变量。该算法的目标是建立一个线性模型&#xff0c;根据输入的自变量来预测一个连续型的因变量。 在线性回归中&#xff0c;我们假设因变量&#xff08;也称为响应变量&#xff09;与…

近80%企业首选——亚马逊云科技为中国企业出海保驾护航

随着全球数字化进程的不断加速&#xff0c;中国出海“大航海时代”已然到来。从#万企组团出国抢订单#到#苏州赴日包机抢单20亿元#&#xff0c;中国企业对海外市场的优势已经一步步建立了起来。 从卖小商品、卖鞋的“世界工厂”&#xff0c;到现在产业升级后的卖汽车、卖服务、…

抖音seo矩阵系统源码|需求文档编译说明(一)

抖音seo矩阵系统文章目录技术囊括 ①产品原型 ②需求文档 ③产品流程图 ④部署方式说明 ⑤完整源码 ⑥源码编译方式说明 ⑦三方框架和SDK使用情况说明和代码位置 ⑧平台操作文档 ⑨程序架构文档 短视频矩阵系统源码开发锦囊囊括前言一、短视频账号矩阵系统开发者必备能力语言&…

招标投标管理微信小程序解决方案

招投标管理微信小程序是一种基于微信公众平台构建的在线招投标管理平台&#xff0c;适用于各类招投标项目管理&#xff0c;通过小程序内的功能实现投标、查看、评估和管理等各项业务。下面我们来了解一下招投标管理微信小程序的具体功能和应用情况。 招投标管理微信小程序的功能…

App 启动速度优化

前言​​​​​​​ APP打开的一瞬间速度快慢&#xff1b;就好比人的第一印象&#xff0c;快速的打开一个应用往往给人很舒服的体验。app经常性卡顿启动速度很慢&#xff0c;这无疑是对用户的流失。 启动方式介绍 APP启动的方式分为3种&#xff1a;冷启动、热启动、温启动。…

28.vite

目录 1 一些概念 1.1 单页面应用程序SPA 1.2 vite 2 初始化vite项目 3 项目中的文件 1 一些概念 1.1 单页面应用程序SPA 单页面应用程序是只有一个页面的前端&#xff0c;切换页面通过前端路由来切换 特点如下 实现了前后端分离&#xff0c;后端仅出接口&#…

Flink TableAPI window and watermarket

序言 本次主要是弄清楚.批流统一 的处理方式,因为它是使用SQL来操作批流计算的.所以它怎么设置算子并行度?如何设置窗口?如何处理流式数据?等等 有很多疑问. 我还是觉得直接使用流计算的API更好.流批一体API最终也是转换成流式计算,最主要的是使用sql来设置算子或者窗口,并…

python合并多个excel,每个excel中有相同的列,按指定列名将数据列合并到一起。以统计学生多个作业提交情况为例。

一、实现目标 有多个excel文件,每个excel文件是一次学生作业的提交情况,最终统计出所有学生所有作业的提交情况。具体格式和内容如下: excel1.xlsx excel2.xlsx excel3.xlsx: 最后统计出所有学生提交的所有作业的情况: 二、实现思路

C# 自动备份文件

目录 文件目录如下 APBackUpFiles app.config OracleHelper LocalFileMethods LogFile packages.config ReadFile 如何发布 在工作的时候&#xff0c;遇到了需要定时对服务器的文件进行备份的需求&#xff0c;原因是 AP&#xff08;服务器&#xff09;上的空间不够了&a…

遗传算法解决TSP旅行商问题(numpy、pandas)

努力是为了不平庸~ 学习的最大理由是想摆脱平庸&#xff0c;早一天就多一份人生的精彩&#xff1b;迟一天就多一天平庸的困扰。 目录 一、引言 原理&#xff1a; 问题&#xff1a; 二、思路步骤 三、代码编写步骤 A、代码各步骤的方法、目的及意义 1. 导入所需的库&…

测试人,你凭什么脱颖而出?

我们在软件测试面试时&#xff0c;可能经常会碰到HR这样问“与其他竞争者相比&#xff0c;你认为自己的优势在哪里&#xff1f;” 看似简单&#xff0c;但仔细深思可能心理陡然冰冰凉&#xff0c;因为自己难以有信心比他人突出&#xff08;除了腰间盘&#xff09;&#xff0c;看…

DBA 抓包神器 tshark 测评

想窥探神秘的网络世界的奥秘&#xff0c;tshark 助你一臂之力&#xff01; 作者&#xff1a;赵黎明 爱可生 MySQL DBA 团队成员&#xff0c;熟悉 Oracle、MySQL 等数据库&#xff0c;擅长数据库性能问题诊断、事务与锁问题的分析等&#xff0c;负责处理客户 MySQL 及我司自研 D…