本文讲解如何使用Winsock2接收一条来自浏览器的HTTP报文并进行回显。
TCP/IP协议的核心内容被封装在了操作系统中,程序员想要使用TCP/IP协议来实现自己的功能时,只能通过系统提供的接口来实现,这个接口我们叫做套接字接口。
TCP是传输层一个面向连接的协议。在通信双方通信前,必须连接上了才行。而说到连接,究竟是谁和谁进行连接,我们用了一个五元组来描述(由于是端到端,所以必须有端口号):
<协议,本地ip地址,本地端口号,远程ip地址,远程端口号>
我们在进行编程实际上就是:填满五元组,让通信请求方(客户端)能够将通信请求发送到正确的位置,而通信接受方(服务端)则需要在正确的位置接收到客户端的请求,并响应连接请求。
如果只是要做一个Web服务器,我们只需要关注上图的左半部分,它描述了在socket编程中,服务端怎么接收到客户端的发来的连接请求并进行连接。当然,学习右半部分可以让你对整个通信过程有更好的把握。(先大概浏览一遍,在下面的代码实现中真正体会这张图即可)
类声明
这个文件声明了类HttpServer。想要知道这些变量都有什么作用,不如直接看类实现部分。
//
// Created by 陈浩楠 on 2023/3/23.
//
#ifndef HTTPSERVER_HTTPSERVER_H
#define HTTPSERVER_HTTPSERVER_H
#include <Winsock2.h>
#include <iostream>
#include <string>
#pragma comment(lib,"ws2_32.lib")
const int BUFF_LEN(2048);
class HttpServer
{
private:
//用于监听客户请求了服务端的某个端口
std::string serverIP;
unsigned short serverPort;
//用于接收客户端发来的HTTP报文
char receiveBuff[BUFF_LEN];
//一些Winsock需要的中间量
WSAData wsaData;
SOCKET listenSocket,acceptSocket;
struct sockaddr_in addrServer,addrClient; //Client是用于接收连接时保存客户端信息的
//socketaddr的长度
int len;
//挂起的连接队列长度
int backlog;
//中间函数
int receiveMessage();
public:
HttpServer(const std::string serverIP,const unsigned short serverPort);
int start();
};
#endif //HTTPSERVER_HTTPSERVER_H
类实现
构造函数
在这个函数中,我们接收了用户传入的本地ip和本地监听端口并将它们赋值给类变量,也就是得到五元组中的两元了。
然后我们又将本地ip和监听端口存入了一个结构体中,并指定了协议族AF_INET
,也就是ipv4的意思。
注意这里我们用了一个inet_addr
函数,它可以将点分十进制的ip地址转换为网络字节序列的长整型(string→long)。在存入端口的时候使用了一个htons
函数,这是将主机字节序的端口号转换为网络字节序。
在计算机网络中,不同的计算机和操作系统使用不同的字节序(即数据在内存中的存储顺序)。主机字节序(也称为本地字节序)是指在当前系统的内存中使用的字节序,而网络字节序(也称为大端字节序)是一个标准的字节序,它用于在网络上传输数据。
在网络通信中,由于不同的计算机和操作系统使用不同的字节序,如果不进行转换,发送方和接收方之间的数据交换可能会出现错误。为了解决这个问题,发送方必须将数据转换为网络字节序,接收方收到数据后再将其转换回主机字节序。
最下面两行还不重要,先不用管。
HttpServer::HttpServer(const std::string serverIP, const unsigned short serverPort)
{
this->serverIP=serverIP;
this->serverPort=serverPort;
addrServer.sin_addr.S_un.S_addr= inet_addr(serverIP.c_str());
addrServer.sin_family=AF_INET;
addrServer.sin_port= htons(serverPort);
len=sizeof(sockaddr);
backlog=1;
}
start函数
这个函数用于建立TCP连接并接收一个来自客户端的信息。
这部分操作和上面的那张大图都可以一一对应上。
首先,检查协议栈,也就是检查系统协议栈安装情况,socket编程要求你检查通过了才能继续其他操作。
if (SOCKET_ERROR== WSAStartup(MAKEWORD(2,2),&wsaData))
{
cout<<"WSAStartup 错误"<<endl;
return -1;
}
从操作系统那获取到一个套接字接口,
第一个参数是指定要什么通信协议,这里使用的是ipv4协议。
套接口有多种类型,第二个参数便是指定要什么类型的套接口,我们这里使用的是流式套接字。
第三个参数是指定某个协议的特定类型。由于我们用的TCP协议只有一个协议字段,这个参数就可以填0。
if (INVALID_SOCKET==(listenSocket= socket(AF_INET,SOCK_STREAM,0)))
{
cout<<"socket 失败"<<endl;
WSACleanup();
return -1;
}
将ip地址,协议族,端口号绑定到listenSocket上。第一个参数是要被绑定东西的socket,第二个参数是一个结构体sockaddr,第三个参数是这个结构体的长度。(这就像我们传一个数组进函数时总是得传数组长度进去一样的道理)
if (INVALID_SOCKET== bind(listenSocket,(sockaddr *)&addrServer,len))
{
cout<<"bind 失败"<<endl;
closesocket(listenSocket);
WSACleanup();
return -1;
}
监听端口。第一个参数是用来做监听的socket,第二个参数是等待队列长度。假设现在有100个客户端线程请求连接到我们这个端口,而我们只能处理20个,那么我们就会将前20个连接请求放在“等待处理”队列中,而剩下的80个请求会造成一个WSAECONNREFUSED
错误。而这里定义的"只能处理20个"里的20就是backlog。
if (INVALID_SOCKET== listen(listenSocket,backlog))
{
cout<<"listen 失败"<<endl;
closesocket(listenSocket);
WSACleanup();
return -1;
}
接收连接。listen是一个阻塞函数,当它运行结束就代表有连接请求成功发送到监听端口。我们只要在listen下面尽管接收连接即可。
注意这里又出现了一个socket(acceptSocket)。这是因为在TCP协议中,服务器需要先监听一个特定的端口,等待客户端的连接请求,一旦有客户端连接请求到来,服务器就需要使用accept函数接受该连接,建立起一个新的套接字(acceptSocket)来和客户端进行通信。
当服务器使用socket函数创建一个监听套接字(listenSocket)时,该套接字只是用来接受客户端连接请求的,它并不直接用于实际的数据传输。一旦有客户端连接请求到来,服务器就需要使用accept函数接受该连接,建立起一个新的套接字(acceptSocket)来和客户端进行通信。这个新的套接字就是服务器和客户端之间实际进行数据传输的套接字,而服务器的监听套接字则继续等待下一个客户端连接请求。
if (SOCKET_ERROR==(acceptSocket=accept(listenSocket,(sockaddr *)&addrClient,&len)))
{
cout<<"accept 失败"<<endl;
closesocket(listenSocket);
WSACleanup();
return -1;
}
然后接收一条来自客户端的HTTP报文,到这一步我们就实现了我们要做的所有事情。
最后,我们需要关闭打开的套接字,释放Windows DLL资源。
receiveMessage();
closesocket(acceptSocket);
closesocket(listenSocket);
WSACleanup();
return 0;
receiveMessage函数
这个函数接收一条来自客户端的信息并进行回显。
首先使用memset
函数清空接收缓冲区。
然后使用recv
函数接收来自客户端的报文段到接收缓冲区中。
最后将接收缓冲区中的内容打印出来。
int HttpServer::receiveMessage()
{
memset(receiveBuff,0,BUFF_LEN);
int ret=recv(acceptSocket,receiveBuff,BUFF_LEN-1,0);
if (SOCKET_ERROR==ret)
{
cout<<"recv错误"<<endl;
return -1;
}
else
{
cout<<"客户端发来了请求:"<<endl;
cout<<receiveBuff<<endl;
return 0;
}
}
结果演示
由于我监听的是81号端口,所以当我在浏览器敲下以下内容并按下回车后,我的程序就会接收到一条来自浏览器的HTTP请求报文并进行回显。
localhost:81
就像这样:
参考文献
-
[1] chatgpt3.5
完整代码
HttpServer.h
//
// Created by 陈浩楠 on 2023/3/23.
//
#ifndef HTTPSERVER_HTTPSERVER_H
#define HTTPSERVER_HTTPSERVER_H
#include <Winsock2.h>
#include <iostream>
#include <string>
#pragma comment(lib,"ws2_32.lib")
const int BUFF_LEN(2048);
class HttpServer
{
private:
//用于监听客户请求了服务端的某个端口
std::string serverIP;
unsigned short serverPort;
//用于接收客户端发来的HTTP报文
char receiveBuff[BUFF_LEN];
//一些Winsock需要的中间量
WSAData wsaData;
SOCKET listenSocket,acceptSocket;
struct sockaddr_in addrServer,addrClient; //Client是用于接收连接时保存客户端信息的
//socketaddr的长度
int len;
//挂起的连接队列长度
int backlog;
//中间函数
int receiveMessage();
public:
HttpServer(const std::string serverIP,const unsigned short serverPort);
int start();
};
#endif //HTTPSERVER_HTTPSERVER_H
HttpServer.cpp
//
// Created by 陈浩楠 on 2023/3/23.
//
#include "HttpServer.h"
using std::cout;
using std::endl;
HttpServer::HttpServer(const std::string serverIP, const unsigned short serverPort)
{
this->serverIP=serverIP;
this->serverPort=serverPort;
addrServer.sin_addr.S_un.S_addr= inet_addr(serverIP.c_str());
addrServer.sin_family=AF_INET;
addrServer.sin_port= htons(serverPort);
len=sizeof(sockaddr);
backlog=1;
}
int HttpServer::start()
{
// 控制台显示乱码纠正,去掉不影响运行(来源网络)
system("chcp 65001"); //设置字符集 (使用SetConsoleCP(65001)设置无效,原因未知)
CONSOLE_FONT_INFOEX info = { 0 };
info.cbSize = sizeof(info);
info.dwFontSize.Y = 16; // leave X as zero
info.FontWeight = FW_NORMAL;
wcscpy(info.FaceName, L"Consolas");
SetCurrentConsoleFontEx(GetStdHandle(STD_OUTPUT_HANDLE), NULL, &info);
//检查这个Window套接字版本(2.2版本)能不能用(也就是检查系统协议栈安装情况),能用才能继续其他 Windows 套接字的调用
if (SOCKET_ERROR== WSAStartup(MAKEWORD(2,2),&wsaData))
{
cout<<"WSAStartup 错误"<<endl;
return -1;
}
//获取一个socket
if (INVALID_SOCKET==(listenSocket= socket(AF_INET,SOCK_STREAM,0)))
{
cout<<"socket 失败"<<endl;
WSACleanup();
return -1;
}
//将ip地址,协议族,端口号绑定到listenSocket上
if (INVALID_SOCKET== bind(listenSocket,(sockaddr *)&addrServer,len))
{
cout<<"bind 失败"<<endl;
closesocket(listenSocket);
WSACleanup();
return -1;
}
//监听端口
if (INVALID_SOCKET== listen(listenSocket,backlog))
{
cout<<"listen 失败"<<endl;
closesocket(listenSocket);
WSACleanup();
return -1;
}
//监听到请求就接收它的连接
if (SOCKET_ERROR==(acceptSocket=accept(listenSocket,(sockaddr *)&addrClient,&len)))
{
cout<<"accept 失败"<<endl;
closesocket(listenSocket);
WSACleanup();
return -1;
}
//接收一条HTTP报文并输出
receiveMessage();
//关闭打开的socket
closesocket(acceptSocket);
closesocket(listenSocket);
WSACleanup();
return 0;
}
int HttpServer::receiveMessage()
{
memset(receiveBuff,0,BUFF_LEN);
int ret=recv(acceptSocket,receiveBuff,BUFF_LEN-1,0);
if (SOCKET_ERROR==ret)
{
cout<<"recv错误"<<endl;
return -1;
}
else
{
cout<<"客户端发来了请求:"<<endl;
cout<<receiveBuff<<endl;
return 0;
}
}
main.cpp
#include "HttpServer.h"
int main()
{
HttpServer httpServer("127.0.0.1",81);
httpServer.start();
return 0;
}
BUFF_LEN-1,0);
if (SOCKET_ERROR==ret)
{
cout<<"recv错误"<<endl;
return -1;
}
else
{
cout<<"客户端发来了请求:"<<endl;
cout<<receiveBuff<<endl;
return 0;
}
}
main.cpp
#include "HttpServer.h"
int main()
{
HttpServer httpServer("127.0.0.1",81);
httpServer.start();
return 0;
}