myserver-helloword-helloword
介绍
- ⽬标:学习使⽤C++和socket编程建⽴⼀个基础Web服务器。
- 内容概述:
- 理解Web服务器⼯作原理。
- 学习创建和监听socket。
- 掌握接收和响应HTTP请求的⽅法。
⼯作原理(重要)
- 监听端⼝
- Web服务器在⽹络中开放⼀个或多个端⼝(如HTTP的默认端⼝80),⽤于监听客⼾端的请求。
- 当服务器绑定到特定端⼝后,它会持续检测该端⼝是否有传⼊的连接请求。
- 处理请求
- 当接收到请求时,服务器⾸先解析HTTP请求头和请求体的内容。
- 解析过程包括提取请求的⽅法(GET、POST等)、⽬标资源的URI、HTTP版本和请求头信等。
- 服务器还可以处理附加数据,例如在POST请求中提交的表单数据或上传的⽂件。
- 发送响应
- 根据请求的类型和资源,服务器确定响应的内容。这可能是HTML⻚⾯、图像、样式表、脚本⽂件或其他类型的数据。
- 服务器⽣成HTTP响应消息,包括状态⾏(如HTTP/1.1 200 OK)、响应头(如内容类型、内容⻓度等)和响应体(请求的资源或错误信息)。
- 最后,服务器通过与客⼾端建⽴的连接发送这个HTTP响应。
重要组成
- 静态内容处理:直接从服务器⽂件系统中提供⽂件,如HTML⽂件、图像、样式表和JavaScript⽂件。服务器直接从硬盘读取并发送回客⼾端。
- 动态内容⽣成:运⾏应⽤程序或脚本来动态⽣成⽹⻚内容,
- 服务器会将请求传递给应⽤程序服务器或脚本引擎(如PHP解释器、Java Servlet容器、Python WSGI应⽤服务器等),由它们处理业务逻辑并⽣成最终的HTTP响应内容。
- 安全性管理:包括加密通信(HTTPS)、⾝份验证、访问控制等。
- ⽀持HTTPS加密通信,通过SSL/TLS协议确保传输数据的安全性。
- 实现⽤⼾⾝份验证机制,例如基本认证、摘要认证、JWT token验证等。
- 实施访问控制策略,基于⻆⾊的权限管理(RBAC)以保护敏感资源不被未授权访问
- ⽇志记录:记录服务器活动,如访问记录、错误信息等,⽤于监控和调试。
⽤到的C++语法知识
类型别名与 std::function 模板
1 | // 声明⼀个 std::function 实例,表⽰它可以存储接受两个整数参数并返回⼀个整数的可调⽤对象 |
计算机通信的完整过程
- DHCP 初始化(获取IP地址)
- 请求过程:Bob的便携机连接到⽹络时,因为没有IP地址,所以⾸先会向⽹络发送⼀个DHCP请报⽂(⽬的端⼝67,源端⼝68),这是⼀个⼴播请求。
- 报⽂内容:报⽂包含了便携机的MAC地址,以0.0.0.0为源IP,⽬标IP为⼴播地址255.255.255.255。
- DHCP服务器的响应:⽹络中的DHCP服务器接收到该请求,提供⼀个IP地址给Bob的便携机,同时提供⼦⽹掩码、默认⽹关、DNS服务器地址等信息。
- IP分配确认:便携机从DHCP服务器收到⼀个包含分配的IP地址的DHCP ACK报⽂后,确认使⽤该IP。
- DNS查询(解析域名)
- 请求过程:在获取IP地址后,Bob的便携机需要访问www.google.com,但它还不知道这个域名对应的IP地址,因此需要通过DNS进⾏域名解析。
- DNS查询报⽂:便携机会发送⼀个DNS查询报⽂(UDP协议,⽬的端⼝53)到DNS服务器的IP地址,该报⽂包含了需要解析的域名。
- DNS服务器的响应:DNS服务器会在收到查询报⽂后,返回相应的IP地址给Bob的便携机。
- ARP请求(获取MAC地址)
- ARP查询:为了与默认⽹关进⾏通信,Bob的便携机需要知道⽹关的MAC地址。此时便携机会发送⼀个
- ARP查询报⽂(⽬标MAC地址为⼴播地址FF:FF:FF:FF:FF:FF)到⽹络中,以请求⽹关的MAC地址。
- ARP响应:⽹关收到ARP请求后,返回⼀个ARP响应报⽂,包含⽹关的MAC地址。便携机接收到该报⽂后,记录下⽹关的MAC地址。
- TCP三次握⼿
- TCP连接建⽴:在获取到www.google.com的IP地址后,Bob的便携机需要与该IP建⽴TCP连接。为了建⽴连接,便携机会⾸先发送⼀个TCP SYN报⽂。
- 三次握⼿过程:SYN报⽂到达⽬标服务器后,服务器会返回⼀个SYN-ACK报⽂;便携机收到SYN-ACK后,再发送⼀个ACK报⽂确认连接的建⽴。三次握⼿完成后,TCP连接正式建⽴。
- HTTP请求和响应
- HTTP请求:建⽴TCP连接后,Bob的便携机发送HTTP GET请求来请求⽹⻚内容,该请求报⽂包含了⽬标URL等信息。
- 服务器响应:⽬标服务器接收到HTTP请求后,处理请求并返回⼀个HTTP响应报⽂,其中包含所请求的⽹⻚内容。
- 数据传输和关闭连接:便携机接收到HTTP响应后,开始呈现⽹⻚内容。在数据传输完成后,TCP连接会通过四次挥⼿来关闭连接。
Socket编程基础
socket简介:
在⽹络编程中,socket是⼀个重要概念,⽤于描述IP地址和端⼝,是⽹络数据传输的端点。通过创建socket,计算机之间可以相互发送和接收数据,实现⽹络通信。
- 端⼝是指⽹络通信中的⼀个虚拟通道,⽤于标识不同的服务或应⽤程序。每个端⼝都有⼀个唯
⼀的号码,范围从0到65535。 - 服务器监听特定的端⼝,通常是80端⼝(HTTP协议的默认端⼝)或443端⼝(HTTPS协议的默
认端⼝) - 对于客⼾端和服务器之间的通信,默认会假设HTTP服务运⾏在80端⼝,⽽HTTPS服务运⾏
在443端⼝。因此,如果浏览器访问 http://example.com/ 或
https://example.com/ 时,它会⾃动尝试连接到该域名对应的80端⼝或443端⼝。 - 功能:socket作为⽹络通信的基础,提供了建⽴⽹络连接、数据传输等功能。
Socket类型
流式Socket(SOCK_STREAM)
- ⽤于创建TCP连接。
- 保证数据完整性和顺序性。
- 适⽤于要求可靠传输的应⽤,如Web服务器、⽂件传输等。
数据报Socket(SOCK_DGRAM)
- ⽤于创建UDP连接。
- 不保证数据的顺序和可靠性。
- 适⽤于实时应⽤,如在线游戏、实时视频会议等。
TCP连接流程详解:
服务器端(Server):
- 创建套接字(create):通过系统调⽤ socket() 函数,指定协议类型(如AF_INET或
AF_INET6)、传输层协议(如SOCK_STREAM表⽰TCP)来创建⼀个⽤于⽹络通信的套接字。 - 绑定端⼝号(bind):使⽤ bind() 函数将套接字与特定的IP地址和端⼝号关联起来,这样客⼾端可以通过这个端⼝号找到并连接到服务器。
- 监听连接(listen):调⽤ listen() 函数使服务器端的套接字进⼊被动监听状态,等待来⾃客⼾端的连接请求。
- 接受连接请求(accept):当有新的连接请求时, accept() 函数会阻塞并返回⼀个新的套接字,该新套接字专⻔⽤来与发起连接的客⼾端进⾏数据交换。
- 接收/发送数据(recv/send):通过返回的新套接字,服务器使⽤ recv() 和 send() 函数与客⼾端进⾏全双⼯的数据传输。
- 关闭套接字(close):完成数据交互后,调⽤ close() 函数关闭已建⽴连接的套接字。
客⼾端(Client): - 创建套接字(create):与服务器相同,客⼾端也需要先创建⼀个套接字。
- 发起连接请求(connect):客⼾端调⽤ connect() 函数主动向服务器发起连接请求,并提供服务器的IP地址和端⼝号。
- 发送/接收数据(send/recv):⼀旦连接建⽴成功,客⼾端同样可以使⽤ send() 和 recv() 函数与服务器交换数据。
- 关闭套接字(close):在数据交换完成后,客⼾端关闭其使⽤的套接字以释放资源
UDP连接流程简介:
由于UDP是⽆连接的,因此没有“监听”和“接受连接”的概念。但仍有以下基本步骤:
服务器端(Server):
- 创建套接字(create):与TCP⼀样,⾸先创建⼀个UDP套接字。
- 绑定端⼝号(bind):通过 bind() 函数将套接字绑定到特定的本地IP地址和端⼝上,以便接收来⾃客⼾端的消息。
- 接收/发送消息(recvfrom/sendto):对于UDP,服务器和客⼾端都使⽤ recvfrom() 和sendto() 函数直接读写数据,它们不仅负责数据传输,还需要处理源和⽬标地址信息。
- 关闭套接字(close):在不需要继续接收或发送数据时,关闭套接字以释放资源。
客⼾端(Client): - 创建套接字(create):同样创建⼀个UDP套接字。
- 发送/接收消息(sendto/recvfrom):客⼾端可以直接使⽤ sendto() 向任意服务器发送数据,并使⽤ recvfrom() 接收从服务器或其他客⼾端发来的数据。
- 关闭套接字(close):完成数据交互后,客⼾端关闭其套接字。
Socket编程流程
- 创建Socket:使⽤ socket() 函数创建新的socket。
- 绑定Socket:使⽤ bind() 函数将socket与特定的IP地址和端⼝号绑定,这样socket才能监听来⾃该地址和端⼝的⽹络请求。
- 监听连接:对于服务端,使⽤ listen() 函数让socket进⼊监听状态,等待客⼾端的连接请求。
- 接收连接:使⽤ accept() 函数接受客⼾端的连接请求。
- 数据交换:使⽤ send() 和 recv() (或 read() 和 write() )在连接的socket上发送和接收数据。
- 关闭Socket:通信结束后,使⽤ close() 关闭socket。
创建和监听socket
socket
1 |
|
- domain :指定协议族,对于IPv4⽹络通信,通常设置为 AF_INET 。
- type :指定套接字类型,对于⾯向连接的流式传输服务(如TCP),设置为 SOCK_STREAM 。
- protocol :指定特定的协议编号,若为0,则会根据 domain 和 type ⾃动选择最合适的协议(对于 AF_INET 和 SOCK_STREAM ,系统会选择TCP协议)。
- 函数返回值是⼀个整数,代表新创建套接字的描述符。如果成功创建套接字,该描述符将⽤于后续的连接、绑定、接收和发送数据等操作;若失败,则返回-1,并可以通过 errno 获取错误代码。
- 定义地址并绑定:
1
2
3
4
5
6
7struct sockaddr_in address; //⽤于互联⽹的地址结构。
address.sin_family = AF_INET; //设置地址族为IPv4
address.sin_addr.s_addr = INADDR_ANY;//允许服务器接受发往本机任何IP的请求
address.sin_port = htons(PORT); //设置端⼝号,htons确保端⼝格式正确。
//将服务器 socket server_fd 绑定到 address 所代表的地址和端⼝上,使服务器能够监听来⾃该地址和端⼝的连接请求
bind(server_fd, (struct sockaddr *)&address, sizeof(address));
listen(server_fd, 3); //让socket进⼊被动监听状态。 - sockaddr_in :⽤于互联⽹的地址结构。
- sin_family :设置地址族为IPv4。
- sin_addr.s_addr :允许服务器接受发往本机任何IP的请求。
- sin_port :设置端⼝号, htons 确保端⼝格式正确。
bind
bind() 函数的作⽤是将指定的套接字与给定的本地地址进⾏关联,也就是为服务器端的套接字分配⼀个本地IP地址和端⼝号。这样当客⼾端发起连接请求时,就知道应该连到哪个IP地址和端⼝了。
1 | bind(server_fd, (struct sockaddr *)&address, sizeof(address)); |
- server_fd :是⼀个已创建好的套接字⽂件描述符,通过调⽤ socket() 函数得到。
- (struct sockaddr *)&address :是指向⼀个已初始化的 sockaddr_in 结构体(或
更通⽤的 sockaddr 结构体)的指针。这个结构体包含了服务器要绑定的本地地址信息,包
括IP地址、端⼝号等。 - sizeof(address) :表⽰上述结构体的⼤⼩。
listen
监听⽹络请求:
1 | listen(server_fd, 3); |
- listen() :让socket进⼊被动监听状态。
- 3 :等待连接队列的最⼤⻓度。
- 具体来说,这⾥的数字表⽰服务器可以同时等待的连接请求的最⼤数量。当服务器正在处理⼀个连接请求时,如果有其他客⼾端的连接请求到达,它们将被放⼊等待队列中,直到服务器有空闲的资源来处理它们。
accept
1 | int new_socket = accept(server_fd, (struct sockaddr *)&client_address,&client_len); |
- 使⽤ accept() 函数等待并接受客⼾端连接请求。
- new_socket :新的socket描述符,专⻔⽤于与连接的客⼾端通信。
在服务器端调⽤ listen() 函数进⼊监听状态后,接下来使⽤ accept() 函数等待并接受客⼾端的连接请求。 accept() 会阻塞直到有新的客⼾端连接到来。 - server_fd :之前创建并进⼊监听状态的服务器套接字描述符。
- (struct sockaddr *)&client_address :是⼀个指向 sockaddr_in 或
sockaddr 结构体的指针,⽤于存储发起连接的客⼾端的地址信息。 - &client_len :是指向⼀个整数的指针,⽤于存储实际返回的客⼾端地址结构体的⼤⼩。当⼀个新的客⼾端连接成功建⽴时, accept() 函数会返回⼀个新的套接字描述符new_socket ,这个新描述符专⽤于与该客⼾端进⾏
处理HTTP请求
- 读取请求:
- 使⽤ read() 函数从socket中读取数据。
- buffer :定义为⼀个⾜够⼤的字符数组(缓冲区),⽤于临时存储从socket中读取的客⼾端数据。
- BUFFER_SIZE :缓冲区⼤⼩,需要确保它⾜以容纳⼀个完整的HTTP请求头。
- read() 函数第三个参数通常减去1,以保证在读取到的数据末尾添加结束符 ‘\0’。read() 函数将从指定的套接字中读取数据,并将其存⼊缓冲区 buffer 中。返回值bytes_received 表⽰实际读取到的字节数。根据读取到的数据内容,服务器可以解析出HTTP请求⽅法、URL、HTTP版本等信息,然后执⾏相应的服务逻辑。
发送HTTP响应
- 响应⽅法:
- 使⽤ write() 或 send() 函数向客⼾端发送响应。
- response :响应内容,包括HTTP状态码和响应体。
完整代码
1 |
|
编译和运⾏
- 步骤:
- 使⽤g++编译: g++ -o server server.cpp
- 运⾏服务器: ./server
测试服务器
- 测试⽅法:
- 使⽤浏览器或curl⼯具测试: http://localhost:8080 或 curl http://localhost:8080
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Ysmmm的快乐小屋!