WEB服务器实现(药品商超)
WEB 服务器(药品商店)及压力测试软件的设计
项目概述:在 C语言环境下,利用socket编程技术设计并制作一款基于HTTP协议的轻量级Web服务器。这个服务器不仅能够处理基本的Web请求,如用户注册、登录、产品罗列、详情页展示等,而且支持多用户实时并发访问。这意味着服务器能够同时处理来自多个客户端的请求,为用户提供快速且流畅的访问体验。
相关功能模块:
- TCP连接响应模块:主要负责设置服务端的IP地址以及端口号等,当监听套接字等到客户端的连接请求,便会生成通信套接字,便于后续的通信
- HTTP报文解析:解析客户端发送的HTTP请求报文,其中包含了其想要的获得的资源信息,当解析完请求报文后,服务器会根据请求内容像客户端发送相应的HTML网页、图片、视频等。
- 网页生成模块:通过用户请求关键字,在本地Sqlite3数据库中查询文字、图片、视频等信息,使用HTML语言动态编写网页。
设计亮点:
- 通过epoll+多线程技术实现多并发服务器:服务器能够充分利用多核CPU的优势,同时处理多个客户端的请求
- 学习webBench开源代码对服务器性能做压力测试
这个项目主要是是基于c语言的轻量级的web服务器,如用户的注册,登录,商品展示和详情页面查看,还可以实现多用户并发访问,利用http协议和socket编程技术,结合epoll和多线程技术实现的服务框架。我的项目主要包含3各模块
tcp连接响应模块:首次初始化服务器的socket,设置服务器的ip地址以及端口号,并监听来自客户端的连接请求,一旦接收到请求,就创建一个新的通信套接字用于该用户的后续通信。「socket( )创建socket套接字;bind( )函数将socket与服务器地址(ip和端口)绑定;listen( ) 监听套接字监听连接请求;accept( )函数接受连接请求,并创建新的socket」
第二个模块HTTP报文解析,解析来自客户端发送的HTTP 请求报文,从其中获取到请求方法例如 (GET,POST,URI,HTTP版本等信息,以及请求头( Content-Type)和请求体 )在项目中我主要是通过字符串处理函数解析的请求行,请求体和请求头的,其中最重要的部分是URI是HTTP协议中用于标识网络资源的字符串,它可以是一个URL或URN。在实际应用中,URL更为常见,因为它不仅能标识资源,还能定位到资源的具体位置。
最后网页生成模块,根据用户请求,从seqlite3数据库中查询相应数据内容,并使用HTML语言动态生成网页内容返回给客户端。
(epoll和webbench) 通过epoll+多线程技术实现多发服务器:
//webBench是一个简单的网站性能开源的压力测试工具,可以模拟多个客户端同时访问网站,用于测试网站在并发压力下的表现。指定测试的URL,并发客户端数量,测试时间等参数。运行webBench命令开始压力测试,根据测试报告分析服务器的响应时间,吞吐量等指标,评估服务器性能。
URI
在HTTP(Hypertext Transfer Protocol,超文本传输协议)中,URI(Uniform Resource Identifier,统一资源标识符)扮演着关键角色,用于唯一地标识网络上的资源。URI是一个字符串,它可以是一个URL(Uniform Resource Locator,统一资源定位符)或者URN(Uniform Resource Name,统一资源名称)。然而,在实际应用中,URI的概念经常被URL所替代,因为URL是URI的一个子集,它除了能够标识资源外,还能定位到资源的具体位置。
URI的组成部分
一个典型的URI(或URL)可以包含多个部分,但并非所有部分都是必需的。这些部分通常遵循以下结构(以URL为例):
<scheme>://<user>:<password>@<host>:<port>/<path>?<query>#<fragment>
- **scheme**(协议):指明了用于访问资源的协议类型,如`http`、`https`、`ftp`等。
- **user:password**(认证信息):可选部分,包含访问资源所需的用户名和密码。出于安全考虑,现代Web应用中很少在URL中直接包含密码。
- **host**(主机名):服务器或资源的域名或IP地址。
- **port**(端口号):可选部分,指定了访问资源的端口。如果不指定,则使用默认端口(如HTTP的80端口,HTTPS的443端口)。
- **path**(路径):资源的路径。在Web服务器中,这通常指向服务器上文件系统中的某个位置。
- **query**(查询字符串):可选部分,用于传递额外的信息给服务器,通常用于动态页面内容的请求。它跟在`?`后面,参数之间用`&`分隔。
- **fragment**(片段标识符):可选部分,用`#`表示,用于指定资源的某个特定部分(如HTML页面中的锚点)。这部分不会发送给服务器。
示例
假设有一个URL如下:
https://user:password@www.example.com:8080/path/to/resource?param1=value1¶m2=value2#section
- **scheme**:`https`
- **user:password**:`user:password`(出于安全考虑,不建议在URL中明文包含密码)
- **host**:`www.example.com`
- **port**:`8080`
- **path**:`/path/to/resource`
- **query**:`param1=value1¶m2=value2`
- **fragment**:`section`
总结
URI是HTTP协议中用于标识网络资源的字符串,它可以是一个URL或URN。在实际应用中,URL更为常见,因为它不仅能标识资源,还能定位到资源的具体位置。了解URI的组成部分有助于更好地理解和使用HTTP协议。
epoll
epoll在web服务器中的具体操作流程是一个高效处理大量并发连接的关键机制,它主要通过Linux内核提供的接口实现。以下是epoll在web服务器中的具体操作流程:
1. 创建epoll实例
-
使用
epoll_create()
或epoll_create1()
函数创建一个epoll实例,该函数返回一个文件描述符,即epoll文件描述符。 -
这个文件描述符用于后续的注册文件描述符和等待事件。
2. 注册文件描述符
-
使用
epoll_ctl()
函数将需要监听的文件描述符(如socket)添加到epoll实例中,并指定需要监听的事件类型(可读 // 可写)(如EPOLLIN表示可读事件)。 -
通过
epoll_ctl()
可以添加、修改或删除epoll实例中的文件描述符及其事件。
3. 等待事件发生
-
调用
epoll_wait()
函数等待epoll实例中的文件描述符上的事件发生。 -
epoll_wait()
会阻塞直到有事件发生或达到指定的超时时间。 -
当事件发生时,
epoll_wait()
会返回触发事件的文件描述符数量,并将事件信息填充到用户提供的数组中。
4. 处理事件
-
遍历
epoll_wait()
返回的事件队列,根据事件类型(如可读、可写等)进行相应的处理。 -
对于可读事件,通常表示有新的客户端连接或客户端发送了数据,web服务器可以读取数据并处理请求。
-
处理完事件后,可以继续调用
epoll_wait()
等待下一轮事件的发生。
5. 循环监听和处理
-
web服务器通过不断循环调用
epoll_wait()
来持续监听和处理事件。 -
这样可以高效地处理大量并发连接,避免了传统的轮询方式带来的性能问题。
6. 边缘触发和水平触发
-
epoll提供了两种触发模式:边缘触发(Edge-Triggered)和水平触发(Level-Triggered)。
-
边缘触发模式只在事件状态发生变化时通知应用程序,适用于非阻塞I/O操作。
-
水平触发模式在文件描述符处于就绪状态时持续通知应用程序,适用于阻塞I/O操作。
-
根据实际需求选择合适的触发模式可以提高web服务器的性能。
7. 清理和关闭
-
在web服务器关闭时,需要调用
close()
函数关闭epoll文件描述符和所有已注册的文件描述符。 -
这可以释放系统资源,避免资源泄漏。
epoll在web服务器中的具体操作流程包括创建epoll实例、注册文件描述符、等待事件发生、处理事件、循环监听和处理以及清理和关闭。通过高效的事件驱动机制,epoll能够在大规模并发的网络应用中提供卓越的性能和可扩展性。这使得epoll成为构建高性能web服务器的理想选择。
- socket接受线程:
C语言为了高并发所以选择了epoll。当程序启动的时候(g_net_update.c文件中main函数,会启动一个thread函数create_accept_task)
这个thread就处理一件事情,只管接收客户端的连接,当有连接进来的时候 通过epoll_ctl函数,把socket fd 加入到epoll里面去,epoll设置监听事件EPOLLIN | EPOLLET;
主要是监听的是加入到epoll中的socket是否可读(因为我的需求是客户端连上了server就会马上向server发送一份数据的)。其它的部分在主线程中处理。- 主线程:
是一个无线循环,epoll_wait 函数相当于把客户端的连接从epoll中拿出来(因为我们监听的是EPOLLIN | EPOLLET)说明这个时候客户端有数据发送过来)。再通过recv_buffer_from_fd 函数把客户端发送过来的数据读出来。然后其他的一切就抛给线程池去处理。
- 线程池:
(代码中我会在池里面创建15个线程) 双向链表。加入线程就是在链表后面加一个链表项,链表的前面会一个一个被拿出来处理。主要是malloc 函数free函数,sem_wait函数sem_post的处理(sem_wait 会阻塞当值大于0是会减一,sem_post是值加一)。typedef void* (FUNC)(void arg, int index);是我们自定义的线程的逻辑处理部分,arg是参数,index是第几个线程处理(我们隐形的给每个线程都标了号),例如代码中的respons_stb_info,更加具体可以看看代码里面是怎么实现的。聪明的你也可以改掉这块的内容改成动态线程池,当某个时刻的处理比较多的时候能够动态的增加线程,而不像我代码里面的是固定的。
- 数据库连接池:按照我的需求在处理客户端请求数据的时候是要访问数据库的。就是一下子创建出一堆的数据连接。要访问数据库的时候先去数据库连接池中找出空闲的连接,具体可以看下代码。使用的时候可以参考下database_process.c文件(代码中数据库连接池和线程池中的个数是一样的)。这里我想说下get_db_connect_from_pool这个函数,我用了随机数,我是为了不想每次都从0开始去判断哪个连接没有用到。为了数据库连接池中的每个链接都能等概率的使用到,具体的还是可以看下代码的实现。
socket步骤
Socket编程的基本概念
Socket:在网络编程中,Socket是一个抽象层,它提供了端到端的通信服务。一个Socket代表了一个连接的一端,这个连接可以是两个应用程序之间的双向数据通信通道。
客户端(Client)和服务器(Server):在Socket编程中,通常有一方是服务提供者(服务器),它监听网络上的特定端口,等待客户端的连接请求;另一方是服务请求者(客户端),它向服务器发起连接请求,并在连接建立后通过该连接与服务器进行数据交换。
TCP(传输控制协议)和UDP(用户数据报协议):Socket编程可以基于TCP或UDP协议。TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议,适合传输大量数据;UDP则是一种无连接的、不可靠的、基于数据报的传输层通信协议,适用于对实时性要求较高但允许少量丢包的应用场景。
Socket编程的步骤
以TCP为例,Socket编程通常包括以下几个步骤:
创建Socket:在客户端和服务器端分别创建一个Socket实例。
绑定(Bind):服务器端通过调用bind()函数将Socket与特定的IP地址和端口号绑定在一起。这一步是服务器端特有的。
监听(Listen):服务器端调用listen()函数使Socket进入被动监听状态,准备接收客户端的连接请求。
建立连接(Connect):客户端通过调用connect()函数向服务器发起连接请求。对于服务器端,这一步是通过accept()函数完成的,它接受客户端的连接请求,并返回一个新的、用于与客户端通信的Socket。
数据交换(Send/Receive):连接建立后,客户端和服务器就可以通过read()和write()(或send()和recv())等函数进行数据交换了。
关闭连接(Close):通信结束后,客户端和服务器都会调用close()函数关闭Socket,释放资源。