Socket编程实验
Socket编程实验
一、实验要求
1.1 实验目标和内容
1.1.1 实验目的
● 了解应用层和运输层的作用及相关协议的工作原理和机制。
● 掌握 SOCKET 编程的基本方法。
1.1.2 实验环境
● 操作系统:Windows
● 语言:C++
● 编程开发环境:Visual Studio 2008-2019 皆可
1.1.3 实验内容
**题目:**编写一个 Web 服务器软件,要求如下:
基本要求:
● 可配置 Web 服务器的监听地址、监听端口和主目录(不得写在代码里面,不能每配置一次都要重编译代码);
● 能够单线程处理一个请求。当一个客户(浏览器,输入 URL:http://202.103.2.3/index.html)连接时创建一个连接套接字;
● 从连接套接字接收 http 请求报文,并根据请求报文的确定用户请求的网页文件;
● 从服务器的文件系统获得请求的文件。 创建一个由请求的文件组成的 http 响应报文。;
● 经 TCP 连接向请求的浏览器发送响应,浏览器可以正确显示网页的内容;
高级要求:
● 能够传输包含多媒体(如图片)的网页给客户端,并能在客户端正确显示;
● 在服务器端的屏幕上输出请求的来源(IP 地址、端口号和 HTTP 请求命令行);
● 在服务器端的屏幕上能够输出对每一个请求处理的结果;
● 对于无法成功定位文件的请求,根据错误原因,作相应错误提示,并具备一定的异常情况处理能力。
1.2 检查表
二、功能实现
2.1 Server和Client简单Demo
2.1.1 Server
#pragma once
#include "winsock2.h"
#include <stdio.h>
#include <iostream>
#include <string>
#include <list>
using namespace std;
#pragma comment(lib,"ws2_32.lib")
//当chrome浏览器打开一个网页时,除了请求网页本身外,还会发出另外一个请求 /favicon.ico,请求谷歌浏览器的图标
//因此Server实际上打开了多个会话Socket,需要一个List来保存这些会话Socket
//保存打开的当前的会话socket
list<SOCKET> sessionSockets;
void main(){
WSADATA wsaData;//描述了关于 Windows Sockects 底层实现的相关信息
fd_set rfds;//用于检查socket是否有数据到来的的文件描述符,用于socket非阻塞模式下等待网络事件通知(有数据到来)
fd_set wfds;//用于检查socket是否可以发送的文件描述符,用于socket非阻塞模式下等待网络事件通知(可以发送数据)
/* 底层初始化 */
int nRc = WSAStartup(0x0202, &wsaData);//初始化
if (nRc) {//初始化失败
printf("Winsock startup failed with error!\n");
}
if (wsaData.wVersion != 0x0202) {//版本号错误
printf("Winsock version is not correct!\n");
}
printf("Winsock startup Ok!\n");
/* Socket初始化 */
//用到的变量:监听Socket、会话Socket、服务器地址、客户端地址、IP地址长度
SOCKET srvSocket;//监听socket
sockaddr_in addr, clientAddr;//服务器地址和客户端地址
SOCKET sessionSocket;//会话socket,负责和client进程通信
int addrLen;//ip地址长度
//创建监听Socket
srvSocket = socket(AF_INET, SOCK_STREAM, 0);
if (srvSocket != INVALID_SOCKET)
printf("Socket create Ok!\n");
//设置服务器的端口和地址
addr.sin_family = AF_INET;
addr.sin_port = htons(5050);
addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY); //主机上任意一块网卡的IP地址
//将监听Socket与主机地址绑定
int rtn = bind(srvSocket, (LPSOCKADDR)&addr, sizeof(addr));
if (rtn != SOCKET_ERROR)
printf("Socket bind Ok!\n");
/* 监听 */
rtn = listen(srvSocket, 5);
if (rtn != SOCKET_ERROR)
printf("Socket listen Ok!\n");
clientAddr.sin_family = AF_INET;
addrLen = sizeof(clientAddr);
/* 设置接收缓冲区 */
char recvBuf[4096];
u_long blockMode = 1;//将srvSock设为非阻塞模式以监听客户连接请求
//调用ioctlsocket,将srvSocket改为非阻塞模式,改成反复检查fd_set元素的状态,看每个元素对应的句柄是否可读或可写
if ((rtn = ioctlsocket(srvSocket, FIONBIO, &blockMode) == SOCKET_ERROR)) {
//FIONBIO:允许或禁止套接口s的非阻塞模式
cout << "ioctlsocket() failed with error!\n";
return;
}
cout << "ioctlsocket() for server socket ok! Waiting for client connection and data\n";
/* 监听Client数据 */
while (true) {
//清空rfds和wfds数组
FD_ZERO(&rfds);
FD_ZERO(&wfds);
//将srvSocket加入rfds数组
//即:当客户端连接请求到来时,rfds数组里srvSocket对应的的状态为可读
//因此这条语句的作用就是:设置等待客户连接请求
FD_SET(srvSocket, &rfds);
for (list<SOCKET>::iterator itor = sessionSockets.begin(); itor != sessionSockets.end(); itor++) {
//将sessionSockets里的每个会话socket加入rfds数组和wfds数组
//即:当客户端发送数据过来时,rfds数组里sessionSocket的对应的状态为可读;
//当可以发送数据到客户端时,wfds数组里sessionSocket的对应的状态为可写
//因此下面二条语句的作用就是:
//设置等待所有会话SOKCET可接受数据或可发送数据
FD_SET(*itor, &rfds);
FD_SET(*itor, &wfds);
}
/*
select工作原理:传入要监听的文件描述符集合(可读、可写,有异常)开始监听,select处于阻塞状态。
当有可读写事件发生或设置的等待时间timeout到了就会返回
返回之前自动去除集合中无事件发生的文件描述符,返回时传出有事件发生的文件描述符集合。
但select传出的集合并没有告诉用户集合中包括哪几个就绪的文件描述符,
需要用户后续进行遍历操作(通过FD_ISSET检查每个句柄的状态)。
*/
//开始等待,等待rfds里是否有输入事件,wfds里是否有可写事件
/*
The select function returns the total number of socket handles
that are ready and contained in the fd_set structure
*/
//返回总共可以读或写的句柄个数
int nTotal = select(0, &rfds, &wfds, NULL, NULL);
//如果srvSock收到连接请求,接受客户连接请求
if (FD_ISSET(srvSocket, &rfds)) {
nTotal--;
//因为客户端请求到来也算可读事件,因此-1,剩下的就是真正有可读事件的句柄个数(即有多少个socket收到了数据)
//产生会话SOCKET
sessionSocket = accept(srvSocket, (LPSOCKADDR)&clientAddr, &addrLen);
if (sessionSocket != INVALID_SOCKET)
printf("Socket listen one client request!\n");
//把会话SOCKET设为非阻塞模式
if ((rtn = ioctlsocket(sessionSocket, FIONBIO, &blockMode) == SOCKET_ERROR)) {
//FIONBIO:允许或禁止套接口s的非阻塞模式。
cout << "ioctlsocket() failed with error!\n";
return;
}
cout << "ioctlsocket() for session socket ok! Waiting for client connection and data\n";
//设置等待会话SOKCET可接受数据或可发送数据
FD_SET(sessionSocket, &rfds);
FD_SET(sessionSocket, &wfds);
//把新的会话socket加入会话socket列表
sessionSockets.push_back(sessionSocket);
}
//检查会话SOCKET是否有数据到来
if (nTotal > 0) { //如果还有可读事件,说明是会话socket有数据到来
//遍历所有会话socket,如果会话socket有数据到来,则接受客户的数据
for (list<SOCKET>::iterator itor = sessionSockets.begin(); itor != sessionSockets.end(); itor++) {
if (FD_ISSET(*itor, &rfds)) { //如果遍历到的当前socket有数据到来
//receiving data from client
memset(recvBuf, '\0', 4096);
rtn = recv(*itor, recvBuf, 4096, 0);
if (rtn > 0) {
printf("Received %d bytes from client: \n%s\n", rtn, recvBuf);
//新增加的:2022-10-26
//向服务器发送响应
if (FD_ISSET(*itor, &wfds)) {//遍如果历到的当前socket可发送数据
//构建响应报文
std::string content = "Welcome to VerySimpleServer";
std::string resp;
resp.append("HTTP/1.1 200 OK\r\n");
resp.append("Server: VerySimpleServer\r\n");
resp.append("Content-Type: text/html; charset=ISO-8859-1\r\n");
resp.append("Content-Length: ").append(std::to_string(content.length())).append("\r\n");
resp.append("\r\n");
resp.append(content);
rtn = send(*itor, resp.c_str(), resp.length(), 0);
if (rtn > 0) {
printf("Send %d bytes to client: %s\n", rtn, resp.c_str());
}
}
}
}
}
}
}
}
上述搭建的服务器支持网页的访问请求,访问网址 http://127.0.0.1,会向该服务器请求网页资源,然后该服务器会发送响应报文,向浏览器推送网页,使用IP地址127.0.0.1是因为该IP地址指向的是本主机,而不会参与到公共网络的访问中去。
2.1.2 Client
#pragma once
#include "winsock2.h"
#include <Ws2tcpip.h>
#include <stdio.h>
#include <iostream>
#include <string>
using namespace std;
#pragma comment(lib,"ws2_32.lib")
void main() {
/* 底层初始化 */
WSADATA wsaData;
string input;
int nRc = WSAStartup(0x0202, &wsaData);
if (nRc) {
printf("Winsock startup failed with error!\n");
}
if (wsaData.wVersion != 0x0202) {
printf("Winsock version is not correct!\n");
}
printf("Winsock startup Ok!\n");
/* Client的Socket初始化 */
SOCKET clientSocket;
sockaddr_in serverAddr, clientAddr;
int addrLen;
//create socket
clientSocket = socket(AF_INET, SOCK_STREAM, 0);
if (clientSocket != INVALID_SOCKET)
printf("Socket create Ok!\n");
//set client port and ip
clientAddr.sin_family = AF_INET;
clientAddr.sin_port = htons(0);
clientAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
//binding
int rtn = bind(clientSocket, (LPSOCKADDR)&clientAddr, sizeof(clientAddr));
if (rtn != SOCKET_ERROR)
printf("Socket bind Ok!\n");
/* Server的Socket初始化 */
//set server's ip and port
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(5050);
inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr.s_addr);
rtn = connect(clientSocket, (LPSOCKADDR)&serverAddr, sizeof(serverAddr));
if (rtn == SOCKET_ERROR)
printf("Connect to server error!\n");
printf("Connect to server ok!");
do {
cout << "\nPlease input your message:";
cin >> input;
//send data to server
rtn = send(clientSocket, input.c_str(), input.length(), 0);
if (rtn == SOCKET_ERROR) {
printf("Send to server failed\n");
closesocket(clientSocket);
WSACleanup();
return;
}
} while (input != "quit");
closesocket(clientSocket);
WSACleanup();
}
该客户端模拟一个向服务器发送消息的进程。
2.2 配置监听地址、端口和主目录
配置监听IP地址、端口和主目录,改变上述配置之后不允许重新编译,那么我们可以通过添加一个配置文件,将Server的监听地址、端口和主目录存储在配置文件中,通过读文件的方式来获取设置的监听地址、端口和主目录。
2.2.1 Server
配置文件的存储格式如下:
读取配置文件并解析的代码如下:
//读取配置文件,对监听地址、端口、主目录进行配置
string IP;//服务器IP地址
string catalogue;//主目录
short port;//端口
ifstream config;
config.open("E:/Lab1_Server/config.txt", ios::in);
if (!config.is_open()) {//打开配置文件失败
cout << "Open config file failed! " << endl;
}
else {
cout << "Start to analyse the config file..."<<endl;
char buf[1024];
memset(buf, '\0', 1024);
while(config.getline(buf, sizeof(buf))){
string s(buf);
if (s.find("IP") != string::npos) {//IP配置行
IP.assign(s,3);
}
else if (s.find("port") != string::npos) {//端口port配置行
port = short(atoi(s.substr(5).c_str()));
}
else if (s.find("catalogue") != string::npos) {//主目录配置行
catalogue.assign(s, 10);
}
}
}
在配置监听地址与端口处,将我们解析得到的IP与port设置为服务器的IP与port
//设置服务器的端口和地址
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
inet_pton(AF_INET, IP.c_str(), &addr.sin_addr.s_addr);
addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
cout << "Set Server IP success:" << IP << endl;
cout << "Set Server port success:" << port << endl;
经过测试,edge浏览器可以正确得获得网页。
我们修改配置文件 config.txt
中的IP地址和端口号,同样可以实现网页的请求,因此也实现了在监听端口上进行监听的功能,同时网页正常得显式也说明了我们当前的服务器可以在收到客户端请求时创建连接套接字
2.3 响应客户端请求,定位相应的html文件
我们之前反馈给网页的是一个字符串,因此网页只能显示这一句话,注意到网页内容的请求报文为(以edge浏览器为例):
GET / HTTP/1.1
Host: 127.0.0.1:5050
Connection: keep-alive
sec-ch-ua: "Chromium";v="106", "Microsoft Edge";v="106", "Not;A=Brand";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36 Edg/106.0.1370.52
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
网页请求网页图标的请求报文为:
GET /favicon.ico HTTP/1.1
Host: 127.0.0.1:5050
Connection: keep-alive
sec-ch-ua: "Chromium";v="106", "Microsoft Edge";v="106", "Not;A=Brand";v="99"
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36 Edg/106.0.1370.52
sec-ch-ua-platform: "Windows"
Accept: image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: no-cors
Sec-Fetch-Dest: image
Referer: http://127.0.0.1:5050/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
浏览器并不会明确得告诉我们它所要请求的html网页是什么,根据 Accept
语句,它希望得到的是text文本或html文件,因此我们希望将一个存储在主目录下的html文件反馈给网页,首先设计好主目录下的文件结构,如下图所示:
我们将html文件存储在主目录下的 file
文件夹中,将网页中用到的图片存储在 image
文件夹中,实现一个简单的网页:
<html>
<head>
<title>Test Web</title>
</head>
<body>
<div style="text-align: center; width: 100%;">
This is a test HTML
<img src="../image/icon.jpg" />
</div>
</body>
</html>
2.3.1 Server
为了解析请求报文,我们将请求报文按照 \r\n
分割成若干行,存储在全局 vector
中,分割函数如下:
void split(string recvData) {//切割响应报文,按照\r\n分割
string seg("\r\n");//分隔符
int pos = 0;
int size = recvData.size();
for (int i = 0; i < size; ++i) {
pos = recvData.find(seg, i);
if (pos != -1) {
string s = recvData.substr(i, pos - i);
if (s.size() > 0) message.push_back(s);//message为全局vector
i = pos + seg.size() - 1;
}
else break;
}
}
初步编写构造响应报文的函数如下,可以构造相关的html报文:
string create_reponse(string recvData,string catalogue) {//创建响应报文
/*
recvData: 请求报文
catalogue: 主目录
return: 响应报文
*/
string resp;//响应报文
//初始化状态行
const string OK("HTTP/1.1 200 OK\r\n");
const string NOT_FOUND("HTTP/1.1 404 NOT FOUND\r\n");
const string FORBIDDEN("HTTP/1.1 403 FORBIDDEN\r\n");
const string MOVED("HTTP/1.1 301 Moved Permanently\r\n");
const string BAD_REQUEST("HTTP/1.1 400 Bad Request\r\n");
const string WRONG_VERSION("HTTP/1.1 505 HTTP Version Not Supported\r\n");
split(recvData);//将请求报文的每一行分割存储到message中
bool target = false;//请求报文是否指明了请求的信息的相对路径
for (int i = 0; i < message.size(); ++i) {
if (message[i].find("GET") != -1 ||
message[i].find("POST") != -1 ||
message[i].find("HEAD") != -1 ||
message[i].find("PUT") != -1 ||
message[i].find("DELETE") != -1) {//分析请求行中的相对路径
if (message[i].find("/ HTTP/1.1") != -1) {//未指明请求的文件的目录,此时发送我们的默认html网页
target = false;
}
else {//分析出相对路径,结合主目录形成绝对路径
}
}
if (message[i].find("Accept:") != -1) {//分析Accept请求字符串
if (message[i].find("text/html") != -1) {//请求html网页或文本
if (!target) {//发送默认html网页
//读取本地html文件
string content;
ifstream web;
web.open("file/web.html", ios::in);
char buf[1024];
memset(buf, '\0', 1024);
while (web.getline(buf, sizeof(buf))) {
content.append(buf);
}
//构造响应报文
resp.append(OK);
resp.append("Server: VerySimpleServer\r\n");
resp.append("Content-Type: text/html; charset=ISO-8859-1\r\n");
resp.append("Content-Length: ").append(to_string(content.length())).append("\r\n");
resp.append("\r\n");
resp.append(content);
break;
}
else {//发送指定的html网页
}
}
else if (message[i].find("image") != -1) {//请求图片
}
}
}
return resp;
}
当我们向浏览器发送构造的响应报文时,我们收到的请求报文中多了这样一条:
GET /image/icon.jpg HTTP/1.1
Host: 127.0.0.1:5050
Connection: keep-alive
sec-ch-ua: "Chromium";v="106", "Microsoft Edge";v="106", "Not;A=Brand";v="99"
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36 Edg/106.0.1370.52
sec-ch-ua-platform: "Windows"
Accept: image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: no-cors
Sec-Fetch-Dest: image
Referer: http://127.0.0.1:5050/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
显然,这正是请求我们html网页中图片的请求报文,说明我们发送html文件的响应报文被浏览器正确接收并解析了,于是我们进一步完善构造响应报文的函数,使其能够发送图片给浏览器,如下:
html = true;
}
else {//分析出相对路径,结合主目录形成绝对路径
int pos_begin = message[0].find("/");
int pos_end = message[0].find(" HTTP/1.1");
route = message[0].substr(pos_begin, pos_end - pos_begin);
if (route.compare("/favicon.ico") == 0) {//单独处理网页图标
route = "/image" + route;
}
route = catalogue + route;
target = true;
cout << "Get the request route:" << route << endl;
if (route.find("html") != -1) {//根据请求的文件名判断浏览器想要什么类型的文件
html = true;
}
else if (route.find("jpg") != -1 ||
route.find("png") != -1 ||
route.find("webp") != -1 ||
route.find("ico") != -1) {
image = true;
}
}
//遍历message,找到Accept:行,解析浏览器可接受的文件类型
for (int i = 0; i < message.size(); ++i) {
if (message[i].find("Accept:") != -1) {//分析Accept请求字符串
if ((message[i].find("text/html") != -1|| message[i].find("*/*")!= -1)&& html) {//请求html网页或文本
if (!target) {//发送默认html网页
//读取本地html文件
string content;
ifstream web;
web.open("file/web.html", ios::in);
char buf[1024];
memset(buf, '\0', 1024);
while (web.getline(buf, sizeof(buf))) {
content.append(buf);
}
//构造响应报文
resp.append(OK);
resp.append("Server: VerySimpleServer\r\n");
resp.append("Content-Type: text/html; charset=ISO-8859-1\r\n");
resp.append("Content-Length: ").append(to_string(content.length())).append("\r\n");
resp.append("\r\n");
resp.append(content);
int rtn = send(s, resp.c_str(), resp.length(), 0);
if (rtn > 0) {
printf("\nSend %d bytes to client: \n%s\n", rtn, resp.c_str());
}
break;
}
else {//发送指定的html网页
break;
}
}
else if ((message[i].find("image/") != -1||message[i].find("*/*") != -1)&&image) {//请求图片
//读取图片文件
string content;
FILE* fp = fopen(route.c_str(), "rb");
fseek(fp, 0, SEEK_END);
int length = ftell(fp);
fseek(fp, 0, SEEK_SET);
//根据图像数据长度分配内存buffer
char* buf = (char*)malloc(length + 1);
memset(buf, '\0', length + 1);
fread(buf, length, 1, fp);
fclose(fp);
//构造响应报文
resp.append(OK);
resp.append("Server: VerySimpleServer\r\n");
resp.append("Content-Type: image/*\r\n");
resp.append("Content-Length: ").append(to_string(length)).append("\r\n");
resp.append("\r\n");
int rtn = send(s, resp.c_str(), resp.length(), 0);
if (rtn > 0) {
printf("\nSend %d bytes to client: \n%s\n", rtn, resp.c_str());
}
rtn = send(s, buf, length, 0);
if (rtn > 0) {
printf("\nSend %d bytes to client: \n%s\n", rtn, buf);
}
break;
}
}
}
message.clear();//注意返回前一定要清空message,以便下次使用
return;
}
可见我们上述的函数大改,从创建报文变为了创建并发送报文,主要是因为image发送一直失败,最后在这个函数中将首部和content分开发送才成功传输了图片,可能是因为缺少了
2.4 支持多种类型的文件传输
在上述html网页中已经实现了html文本和图片的传输,我们首先完善一下浏览器请求访问特定网页的请求,网页内容如下:
<html>
<head>
</head>
<body>
<script>
function clickEffect() {
let balls = [];
let longPressed = false;
let longPress;
let multiplier = 0;
let width, height;
let origin;
let normal;
let ctx;
const colours = ["#F73859", "#14FFEC", "#00E0FF", "#FF99FE", "#FAF15D"];
const canvas = document.createElement("canvas");
document.body.appendChild(canvas);
canvas.setAttribute("style", "width: 100%; height: 100%; top: 0; left: 0; z-index: 99999; position: fixed; pointer-events: none;");
const pointer = document.createElement("span");
pointer.classList.add("pointer");
document.body.appendChild(pointer);
if (canvas.getContext && window.addEventListener) {
ctx = canvas.getContext("2d");
updateSize();
window.addEventListener('resize', updateSize, false);
loop();
window.addEventListener("mousedown", function(e) {
pushBalls(randBetween(10, 20), e.clientX, e.clientY);
document.body.classList.add("is-pressed");
longPress = setTimeout(function(){
document.body.classList.add("is-longpress");
longPressed = true;
}, 500);
}, false);
window.addEventListener("mouseup", function(e) {
clearInterval(longPress);
if (longPressed == true) {
document.body.classList.remove("is-longpress");
pushBalls(randBetween(50 + Math.ceil(multiplier), 100 + Math.ceil(multiplier)), e.clientX, e.clientY);
longPressed = false;
}
document.body.classList.remove("is-pressed");
}, false);
window.addEventListener("mousemove", function(e) {
let x = e.clientX;
let y = e.clientY;
pointer.style.top = y + "px";
pointer.style.left = x + "px";
}, false);
} else {
console.log("canvas or addEventListener is unsupported!");
}
function updateSize() {
canvas.width = window.innerWidth * 2;
canvas.height = window.innerHeight * 2;
canvas.style.width = window.innerWidth + 'px';
canvas.style.height = window.innerHeight + 'px';
ctx.scale(2, 2);
width = (canvas.width = window.innerWidth);
height = (canvas.height = window.innerHeight);
origin = {
x: width / 2,
y: height / 2
};
normal = {
x: width / 2,
y: height / 2
};
}
class Ball {
constructor(x = origin.x, y = origin.y) {
this.x = x;
this.y = y;
this.angle = Math.PI * 2 * Math.random();
if (longPressed == true) {
this.multiplier = randBetween(14 + multiplier, 15 + multiplier);
} else {
this.multiplier = randBetween(6, 12);
}
this.vx = (this.multiplier + Math.random() * 0.5) * Math.cos(this.angle);
this.vy = (this.multiplier + Math.random() * 0.5) * Math.sin(this.angle);
this.r = randBetween(8, 12) + 3 * Math.random();
this.color = colours[Math.floor(Math.random() * colours.length)];
}
update() {
this.x += this.vx - normal.x;
this.y += this.vy - normal.y;
normal.x = -2 / window.innerWidth * Math.sin(this.angle);
normal.y = -2 / window.innerHeight * Math.cos(this.angle);
this.r -= 0.3;
this.vx *= 0.9;
this.vy *= 0.9;
}
}
function pushBalls(count = 1, x = origin.x, y = origin.y) {
for (let i = 0; i < count; i++) {
balls.push(new Ball(x, y));
}
}
function randBetween(min, max) {
return Math.floor(Math.random() * max) + min;
}
function loop() {
ctx.fillStyle = "rgba(255, 255, 255, 0)";
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let i = 0; i < balls.length; i++) {
let b = balls[i];
if (b.r < 0) continue;
ctx.fillStyle = b.color;
ctx.beginPath();
ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2, false);
ctx.fill();
b.update();
}
if (longPressed == true) {
multiplier += 0.2;
} else if (!longPressed && multiplier >= 0) {
multiplier -= 0.4;
}
removeBall();
requestAnimationFrame(loop);
}
function removeBall() {
for (let i = 0; i < balls.length; i++) {
let b = balls[i];
if (b.x + b.r < 0 || b.x - b.r > width || b.y + b.r < 0 || b.y - b.r > height || b.r < 0) {
balls.splice(i, 1);
}
}
}
}
clickEffect();//调用特效函数
</script>
</body>
</html>
👆网上白嫖的简单炫酷的代码,并不重要,重要的是能否进行传输,完善的代码如下:去掉了target的标识(当时搞的时候没想到多此一举),由此我们基本实现了传输网页与图片的功能。
void send_reponse(string recvData,string catalogue,SOCKET s) {//创建响应报文,并发送
/*
recvData: 请求报文
catalogue: 主目录
return: 响应报文
*/
string resp;//响应报文
//初始化状态行
const string OK("HTTP/1.1 200 OK\r\n");
const string NOT_FOUND("HTTP/1.1 404 NOT FOUND\r\n");
const string FORBIDDEN("HTTP/1.1 403 FORBIDDEN\r\n");
const string MOVED("HTTP/1.1 301 Moved Permanently\r\n");
const string BAD_REQUEST("HTTP/1.1 400 Bad Request\r\n");
const string WRONG_VERSION("HTTP/1.1 505 HTTP Version Not Supported\r\n");
split(recvData);//将请求报文的每一行分割存储到message中
string route;//请求报文请求的文件路径
//根据请求行判断浏览器请求的是什么类型的文件
bool html = false;
bool image = false;
//解析请求行,即message[0]
if (message[0].find("/ HTTP/1.1") != -1) {//未指明路径,发送默认网页
route = route + catalogue + "/file/web.html";
html = true;
}
else {//分析出相对路径,结合主目录形成绝对路径
int pos_begin = message[0].find("/");
int pos_end = message[0].find(" HTTP/1.1");
route = message[0].substr(pos_begin, pos_end - pos_begin);
if (route.compare("/favicon.ico") == 0) {//单独处理网页图标
route = "/image" + route;
}
route = catalogue + route;
cout << "Get the request route:" << route << endl;
if (route.find("html") != -1) {//根据请求的文件名判断浏览器想要什么类型的文件
html = true;
}
else if (route.find("jpg") != -1 ||
route.find("png") != -1 ||
route.find("webp") != -1 ||
route.find("ico") != -1) {
image = true;
}
}
//遍历message,找到Accept:行,解析浏览器可接受的文件类型
for (int i = 0; i < message.size(); ++i) {
if (message[i].find("Accept:") != -1) {//分析Accept请求字符串
if ((message[i].find("text/html") != -1|| message[i].find("*/*")!= -1)&& html) {//请求html网页或文本
//读取本地html文件
string content;
ifstream web;
web.open(route.c_str(), ios::in);
char buf[1024];
memset(buf, '\0', 1024);
while (web.getline(buf, sizeof(buf))) {
content.append(buf);
}
//构造响应报文
resp.append(OK);
resp.append("Server: VerySimpleServer\r\n");
resp.append("Content-Type: text/html; charset=ISO-8859-1\r\n");
resp.append("Content-Length: ").append(to_string(content.length())).append("\r\n");
resp.append("\r\n");
resp.append(content);
int rtn = send(s, resp.c_str(), resp.length(), 0);
if (rtn > 0) {
printf("\nSend %d bytes to client: \n%s\n", rtn, resp.c_str());
}
break;
}
else if ((message[i].find("image/") != -1||message[i].find("*/*") != -1)&&image) {//请求图片
//读取图片文件
string content;
FILE* fp = fopen(route.c_str(), "rb");
fseek(fp, 0, SEEK_END);
int length = ftell(fp);
fseek(fp, 0, SEEK_SET);
//根据图像数据长度分配内存buffer
char* buf = (char*)malloc(length + 1);
memset(buf, '\0', length + 1);
fread(buf, length, 1, fp);
fclose(fp);
//构造响应报文
resp.append(OK);
resp.append("Server: VerySimpleServer\r\n");
resp.append("Content-Type: image/*\r\n");
resp.append("Content-Length: ").append(to_string(length)).append("\r\n");
resp.append("\r\n");
int rtn = send(s, resp.c_str(), resp.length(), 0);
if (rtn > 0) {
printf("\nSend %d bytes to client: \n%s\n", rtn, resp.c_str());
}
rtn = send(s, buf, length, 0);
if (rtn > 0) {
printf("\nSend %d bytes to client: \n%s\n", rtn, buf);
}
break;
}
}
}
message.clear();//注意返回前一定要清空message,以便下次使用
return;
}
我们访问以下网址即可得到特定的html文件:烟花特效
为了完善多类型文件的传输,我希望能正确得传输流媒体文件,因此参考一下网络上的视频传输报文:
2.4.1 Server
参考网络上的报文请求格式,我们也可以实现传输mp4文件的响应报文:
void send_reponse(string recvData,string catalogue,SOCKET s) {//创建响应报文,并发送
/*
recvData: 请求报文
catalogue: 主目录
return: 响应报文
*/
string resp;//响应报文
//初始化状态行
const string OK("HTTP/1.1 200 OK\r\n");
const string NOT_FOUND("HTTP/1.1 404 NOT FOUND\r\n");
const string FORBIDDEN("HTTP/1.1 403 FORBIDDEN\r\n");
const string MOVED("HTTP/1.1 301 Moved Permanently\r\n");
const string BAD_REQUEST("HTTP/1.1 400 Bad Request\r\n");
const string WRONG_VERSION("HTTP/1.1 505 HTTP Version Not Supported\r\n");
split(recvData);//将请求报文的每一行分割存储到message中
string route;//请求报文请求的文件路径
//根据请求行判断浏览器请求的是什么类型的文件
bool html = false;
bool image = false;
bool video = false;
//解析请求行,即message[0]
if (message[0].find("/ HTTP/1.1") != -1) {//未指明路径,发送默认网页
route = route + catalogue + "/file/web.html";
html = true;
}
else {//分析出相对路径,结合主目录形成绝对路径
int pos_begin = message[0].find("/");
int pos_end = message[0].find(" HTTP/1.1");
route = message[0].substr(pos_begin, pos_end - pos_begin);
if (route.compare("/favicon.ico") == 0) {//单独处理网页图标
route = "/image" + route;
}
route = catalogue + route;
cout << "Get the request route:" << route << endl;
if (route.find("html") != -1) {//根据请求的文件名判断浏览器想要什么类型的文件
html = true;
}
else if (route.find("jpg") != -1 ||
route.find("png") != -1 ||
route.find("webp") != -1 ||
route.find("ico") != -1) {
image = true;
}
else if (route.find("mp4")!=-1) {
video = true;
}
}
//遍历message,找到Accept:行,解析浏览器可接受的文件类型
for (int i = 0; i < message.size(); ++i) {
if (message[i].find("Accept:") != -1) {//分析Accept请求字符串
if ((message[i].find("text/html") != -1|| message[i].find("*/*")!= -1)&& html) {//请求html网页或文本
//读取本地html文件
string content;
ifstream web;
web.open(route.c_str(), ios::in);
char buf[1024];
memset(buf, '\0', 1024);
while (web.getline(buf, sizeof(buf))) {
content.append(buf);
}
//构造响应报文
resp.append(OK);
resp.append("Server: VerySimpleServer\r\n");
resp.append("Content-Type: text/html; charset=ISO-8859-1\r\n");
resp.append("Content-Length: ").append(to_string(content.length())).append("\r\n");
resp.append("\r\n");
resp.append(content);
int rtn = send(s, resp.c_str(), resp.length(), 0);
if (rtn > 0) {
printf("\nSend %d bytes to client: \n%s\n", rtn, resp.c_str());
}
break;
}
else if ((message[i].find("image/") != -1||message[i].find("*/*") != -1)&&image) {//请求图片
//读取图片文件
string content;
FILE* fp = fopen(route.c_str(), "rb");
fseek(fp, 0, SEEK_END);
int length = ftell(fp);
fseek(fp, 0, SEEK_SET);
//根据图像数据长度分配内存buffer
char* buf = (char*)malloc(length + 1);
memset(buf, '\0', length + 1);
fread(buf, length, 1, fp);
fclose(fp);
//构造响应报文
resp.append(OK);
resp.append("Server: VerySimpleServer\r\n");
resp.append("Content-Type: image/*\r\n");
resp.append("Content-Length: ").append(to_string(length)).append("\r\n");
resp.append("\r\n");
int rtn = send(s, resp.c_str(), resp.length(), 0);
if (rtn > 0) {
printf("\nSend %d bytes to client: \n%s\n", rtn, resp.c_str());
}
rtn = send(s, buf, length, 0);
if (rtn > 0) {
printf("\nSend %d bytes to client: \n%s\n", rtn, buf);
}
break;
}
else if ((message[i].find("video/") != -1 || message[i].find("*/*") != -1) && video) {
//读取视频文件
string content;
FILE* fp = fopen(route.c_str(), "rb");
fseek(fp, 0, SEEK_END);
int length = ftell(fp);
fseek(fp, 0, SEEK_SET);
//根据图像数据长度分配内存buffer
char* buf = (char*)malloc(length + 1);
memset(buf, '\0', length + 1);
fread(buf, length, 1, fp);
fclose(fp);
//构造响应报文
resp.append(OK);
resp.append("Server: VerySimpleServer\r\n");
resp.append("Content-Type: video/mp4\r\n");
resp.append("Content-Length: ").append(to_string(length)).append("\r\n");
resp.append("\r\n");
int rtn = send(s, resp.c_str(), resp.length(), 0);
if (rtn > 0) {
printf("\nSend %d bytes to client: \n%s\n", rtn, resp.c_str());
}
rtn = send(s, buf, length, 0);
if (rtn > 0) {
printf("\nSend %d bytes to client: \n%s\n", rtn, buf);
}
break;
}
}
}
message.clear();//注意返回前一定要清空message,以便下次使用
return;
}
发现传输文件的过程存在很大部分的共性,不妨整合一下3个部分的传输代码,并添加以下功能:
● 对于一些私人文件(文件名中有private字样),它们可以被路径找到,但是不允许发送,发送403响应报文
● 打开文件失败时应该发送404响应报文
● HTTP版本不为HTTP1.1时应该发送505响应报文
● 服务器未从请求行中解析出有效路径时,说明读不懂报文,发送400响应报文
void send_reponse(string recvData,string catalogue,SOCKET s) {//创建响应报文,并发送
/*
recvData: 请求报文
catalogue: 主目录
return: 响应报文
*/
string resp;//响应报文
//初始化状态行
const string OK("HTTP/1.1 200 OK\r\n");
const string NOT_FOUND("HTTP/1.1 404 NOT FOUND\r\n");
const string FORBIDDEN("HTTP/1.1 403 FORBIDDEN\r\n");
const string MOVED("HTTP/1.1 301 Moved Permanently\r\n");
const string BAD_REQUEST("HTTP/1.1 400 Bad Request\r\n");
const string WRONG_VERSION("HTTP/1.1 505 HTTP Version Not Supported\r\n");
split(recvData);//将请求报文的每一行分割存储到message中
string route;//请求报文请求的文件路径
bool forbidden = false;//请求的文件是否为私有的不可访问的文件
//根据请求行判断浏览器请求的是什么类型的文件
bool html = false;
bool image = false;
bool video = false;
//解析请求行,即message[0]
if (message[0].find("/ HTTP/1.1") != -1) {//未指明路径,发送默认网页
route = route + catalogue + "/file/web.html";
html = true;
}
else if (message[0].find("HTTP/1.1") == -1) {//HTTP版本不对,我只支持HTTP1.1
resp.append(WRONG_VERSION);
int rtn = send(s, resp.c_str(), resp.length(), 0);
if (rtn == SOCKET_ERROR) {//显示处理结果
cout << "Failed to send!" << endl;
}
cout << "505 HTTP Version Not Supported! "<< endl;
if (rtn > 0) {
printf("\nSend %d bytes to client: \n%s\n", rtn, resp.c_str());
}
message.clear();//注意返回前一定要清空message,以便下次使用
return;
}
else {//分析出相对路径,结合主目录形成绝对路径
int pos_begin = message[0].find("/");
int pos_end = message[0].find(" HTTP/1.1");
route = message[0].substr(pos_begin, pos_end - pos_begin);
if (route.compare("/favicon.ico") == 0) {//单独处理网页图标
route = "/image" + route;
}
route = catalogue + route;
cout << "Get the request route:" << route << endl;
if (route.find("html") != -1) {//根据请求的文件名判断浏览器想要什么类型的文件
html = true;
}
else if (route.find("jpg") != -1 ||
route.find("png") != -1 ||
route.find("webp") != -1 ||
route.find("ico") != -1) {
image = true;
}
else if (route.find("mp4")!=-1) {
video = true;
}
}
bool understand = html | image | video;//判断是否读懂了请求报文
if (understand) {//正确解析出了报文中的路径
//根据route打开文件:
FILE* fp = fopen(route.c_str(), "rb");
if (fp == NULL) {//未找到文件
resp.append(NOT_FOUND);
int rtn = send(s, resp.c_str(), resp.length(), 0);
if (rtn == SOCKET_ERROR) {//显示处理结果
cout << "Failed to send!" << endl;
}
cout << "404 NOT FOUND, Can't find the file whose route is:" << route << endl;
if (rtn > 0) {
printf("\nSend %d bytes to client: \n%s\n", rtn, resp.c_str());
}
}
else {//找到了文件
if (route.find("private") != -1) {//请求的是私人的不可发送的文件,403
resp.append(FORBIDDEN);
int rtn = send(s, resp.c_str(), resp.length(), 0);
if (rtn == SOCKET_ERROR) {//显示处理结果
cout << "Failed to send!" << endl;
}
cout << "403 FORBIDDEN, The request is forbidden!" << endl;
if (rtn > 0) {
printf("\nSend %d bytes to client: \n%s\n", rtn, resp.c_str());
}
}
else {//判断了这么多次终于可以发送了
//获取文件大小
fseek(fp, 0, SEEK_END);
int length = ftell(fp);
fseek(fp, 0, SEEK_SET);
//将文件内容存储至缓冲区
char* buf = (char*)malloc(length + 1);
memset(buf, '\0', length + 1);
fread(buf, length, 1, fp);
fclose(fp);
//构造响应报文
resp.append(OK);
resp.append("Server: VerySimpleServer\r\n");
if (html) resp.append("Content-Type: text/html; charset=ISO-8859-1\r\n");
else if(image) resp.append("Content-Type: image/*\r\n");
else if(video) resp.append("Content-Type: video/mp4\r\n");
resp.append("Content-Length: ").append(to_string(length)).append("\r\n");
resp.append("\r\n");
//发送首部
int rtn = send(s, resp.c_str(), resp.length(), 0);
if (rtn == SOCKET_ERROR) {//显示处理结果
cout << "Failed to send!" << endl;
}
if (rtn > 0) {
printf("\nSend %d bytes to client: \n%s\n", rtn, resp.c_str());
}
//发送文件内容
rtn = send(s, buf, length, 0);
if (rtn == SOCKET_ERROR) {//显示处理结果
cout << "Failed to send!" << endl;
}
cout << "200 OK, Success to send the file!" << endl;
if (rtn > 0) {
printf("\nSend %d bytes to client: \n%s\n", rtn, buf);
}
}
}
}
else {//未读懂报文,我只能发html,image,mp4,你请求的文件不是我能读懂的
resp.append(BAD_REQUEST);
int rtn = send(s, resp.c_str(), resp.length(), 0);
if (rtn == SOCKET_ERROR) {//显示处理结果
cout << "Failed to send!" << endl;
}
cout << "400 Bad Request, Can't understand the recvData!"<< endl;
if (rtn > 0) {
printf("\nSend %d bytes to client: \n%s\n", rtn, resp.c_str());
}
}
message.clear();//注意返回前一定要清空message,以便下次使用
return;
}
2.5 服务器屏幕上显示请求来源
我们只需要输出会话Socket的IP地址与端口号即可,请求报文已经输出过了,因此不再单独显示http的请求行
2.5.1 Server
//如果srvSock收到连接请求,接受客户连接请求
if (FD_ISSET(srvSocket, &rfds)) {
nTotal--;
sessionSocket = accept(srvSocket, (LPSOCKADDR)&clientAddr, &addrLen);
if (sessionSocket != INVALID_SOCKET) {
printf("Socket listen one client request!\n");
char str[4096];
cout << "Client IP:" << inet_ntop(AF_INET,&clientAddr.sin_addr,str,sizeof(str)) << endl;
cout << "Client port:" << htons(clientAddr.sin_port) << endl;
}
if ((rtn = ioctlsocket(sessionSocket, FIONBIO, &blockMode) == SOCKET_ERROR)) {
cout << "ioctlsocket() failed with error!\n";
return;
}
cout << "ioctlsocket() for session socket ok! Waiting for client connection and data\n\n";
//设置等待会话SOKCET可接受数据或可发送数据
FD_SET(sessionSocket, &rfds);
FD_SET(sessionSocket, &wfds);
//把新的会话socket加入会话socket列表
sessionSockets.push_back(sessionSocket);
}
2.6 请求处理结果与错误提示
我们在上述的代码中已经基本实现了请求处理结果和错误提示的显示,希望进一步得完善此部分:
● 底层初始化错误时,也进行报错,并展示错误代码:
/* 底层初始化 */ int nRc = WSAStartup(0x0202, &wsaData); if (nRc) { printf("Winsock startup failed with error!\n"); WSAGetLastError(); } if (wsaData.wVersion != 0x0202) { printf("Winsock version is not correct!\n"); WSAGetLastError(); } printf("Winsock startup Ok!\n");
● 初始化Server Socket时,也进行报错,并展示错误代码:
/* Socket初始化 */ SOCKET srvSocket; sockaddr_in addr, clientAddr; SOCKET sessionSocket; int addrLen; //创建监听Socket srvSocket = socket(AF_INET, SOCK_STREAM, 0); if (srvSocket != INVALID_SOCKET) printf("Socket create Ok!\n"); else { cout << "Server Socket create failed with error!" << endl; WSAGetLastError(); }
● Socket与地址和端口绑定时,也进行报错,并展示错误代码:
//将监听Socket与主机地址绑定 int rtn = bind(srvSocket, (LPSOCKADDR)&addr, sizeof(addr)); if (rtn != SOCKET_ERROR) printf("Socket bind Ok!\n"); else { cout << "Socket bind failed with error!" << endl; WSAGetLastError(); }
● 与客户端连接失败时,也进行报错,并展示错误代码:
sessionSocket = accept(srvSocket, (LPSOCKADDR)&clientAddr, &addrLen); if (sessionSocket != INVALID_SOCKET) { printf("Socket listen one client request!\n"); char str[4096]; cout << "Client IP:" << inet_ntop(AF_INET,&clientAddr.sin_addr,str,sizeof(str)) << endl; cout << "Client port:" << htons(clientAddr.sin_port) << endl; } else {//与客户端连接失败 cout << "Failed to connect the client!" << endl; WSAGetLastError(); }
● 针对可复现的404(NOT FOUND)、403(FORBIDDEN)、400(Bad Request)三种错误情况,在发送请求响应报文之后再向客户端反馈一张提示当前遇到的错误的图片,进行错误提示:(没错,我又对函数大改了一下,将处理这三个错误的部分的报文构造封装成了一个函数
error_tips()
)void send_reponse(string recvData,string catalogue,SOCKET s) {//创建响应报文,并发送 /* recvData: 请求报文 catalogue: 主目录 return: 响应报文 */ string resp;//响应报文 //初始化状态行 const string OK("HTTP/1.1 200 OK\r\n"); const string NOT_FOUND("HTTP/1.1 404 NOT FOUND\r\n"); const string FORBIDDEN("HTTP/1.1 403 FORBIDDEN\r\n"); const string MOVED("HTTP/1.1 301 Moved Permanently\r\n"); const string BAD_REQUEST("HTTP/1.1 400 Bad Request\r\n"); const string WRONG_VERSION("HTTP/1.1 505 HTTP Version Not Supported\r\n"); split(recvData);//将请求报文的每一行分割存储到message中 string route;//请求报文请求的文件路径 bool forbidden = false;//请求的文件是否为私有的不可访问的文件 //根据请求行判断浏览器请求的是什么类型的文件 bool html = false; bool image = false; bool video = false; //解析请求行,即message[0] if (message[0].find("/ HTTP/1.1") != -1) {//未指明路径,发送默认网页 route = route + catalogue + "/file/web.html"; html = true; } else if (message[0].find("HTTP/1.1") == -1) {//HTTP版本不对,我只支持HTTP1.1 resp.append(WRONG_VERSION); int rtn = send(s, resp.c_str(), resp.length(), 0); if (rtn == SOCKET_ERROR) {//显示处理结果 cout << "Failed to send!" << endl; } cout << "505 HTTP Version Not Supported! "<< endl; if (rtn > 0) { printf("\nSend %d bytes to client: \n%s\n", rtn, resp.c_str()); } message.clear();//注意返回前一定要清空message,以便下次使用 return; } else {//分析出相对路径,结合主目录形成绝对路径 int pos_begin = message[0].find("/"); int pos_end = message[0].find(" HTTP/1.1"); route = message[0].substr(pos_begin, pos_end - pos_begin); if (route.compare("/favicon.ico") == 0) {//单独处理网页图标 route = "/image" + route; } route = catalogue + route; cout << "Get the request route:" << route << endl; if (route.find("html") != -1) {//根据请求的文件名判断浏览器想要什么类型的文件 html = true; } else if (route.find("jpg") != -1 || route.find("png") != -1 || route.find("webp") != -1 || route.find("ico") != -1) { image = true; } else if (route.find("mp4")!=-1) { video = true; } } bool understand = html | image | video;//判断是否读懂了请求报文 if (understand) {//正确解析出了报文中的路径 //根据route打开文件: FILE* fp = fopen(route.c_str(), "rb"); if (fp == NULL) {//未找到文件 error_tips(s, 404);//反馈错误提示,服务器输出404错误 } else {//找到了文件 if (route.find("private") != -1) {//请求的是私人的不可发送的文件,403 error_tips(s, 403); } else {//判断了这么多次终于可以发送了 //获取文件大小 fseek(fp, 0, SEEK_END); int length = ftell(fp); fseek(fp, 0, SEEK_SET); //将文件内容存储至缓冲区 char* buf = (char*)malloc(length + 1); memset(buf, '\0', length + 1); fread(buf, length, 1, fp); fclose(fp); //构造响应报文 resp.append(OK); resp.append("Server: VerySimpleServer\r\n"); if (html) resp.append("Content-Type: text/html; charset=ISO-8859-1\r\n"); else if(image) resp.append("Content-Type: image/*\r\n"); else if(video) resp.append("Content-Type: video/mp4\r\n"); resp.append("Content-Length: ").append(to_string(length)).append("\r\n"); resp.append("\r\n"); //发送首部 int rtn = send(s, resp.c_str(), resp.length(), 0); if (rtn == SOCKET_ERROR) {//显示处理结果 cout << "Failed to send!" << endl; } if (rtn > 0) { printf("\nSend %d bytes to client: \n%s\n", rtn, resp.c_str()); } //发送文件内容 rtn = send(s, buf, length, 0); if (rtn == SOCKET_ERROR) {//显示处理结果 cout << "Failed to send!" << endl; } cout << "200 OK, Success to send the file!" << endl; if (rtn > 0) { printf("\nSend %d bytes to client: \n%s\n", rtn, buf); } } } } else {//未读懂报文,我只能发html,image,mp4,你请求的文件不是我能读懂的 error_tips(s, 400); } message.clear();//注意返回前一定要清空message,以便下次使用 return; }
void error_tips(SOCKET s,int problem) { //错误提示 const string NOT_FOUND("HTTP/1.1 404 NOT FOUND\r\n"); const string FORBIDDEN("HTTP/1.1 403 FORBIDDEN\r\n"); const string BAD_REQUEST("HTTP/1.1 400 Bad Request\r\n"); //发送错误提示图片——即发送一个包含图片的html网页 string error; string route; if (problem == 404) { route = route + "file/404.html"; cout << "404 NOT FOUND, Can't find the file whose route is:" << route << endl; error.append(NOT_FOUND); } else if (problem == 403) { route = route + "file/403.html"; cout << "403 FORBIDDEN, The request is forbidden!" << endl; error.append(FORBIDDEN); } else if (problem == 400) { route = route + "file/400.html"; cout << "400 Bad Request, Can't understand the recvData!" << endl; error.append(BAD_REQUEST); } FILE* fp = fopen(route.c_str(), "rb"); //获取文件大小 fseek(fp, 0, SEEK_END); int length = ftell(fp); fseek(fp, 0, SEEK_SET); //将文件内容存储至缓冲区 char* buf = (char*)malloc(length + 1); memset(buf, '\0', length + 1); fread(buf, length, 1, fp); fclose(fp); error.append("Server: VerySimpleServer\r\n"); error.append("Content-Type: text/html\r\n"); error.append("Content-Length: ").append(to_string(length)).append("\r\n"); error.append("\r\n"); //发送首部 int rtn = send(s, error.c_str(), error.length(), 0); if (rtn == SOCKET_ERROR) {//显示处理结果 cout << "Failed to send!" << endl; } if (rtn > 0) { printf("\nSend %d bytes to client: \n%s\n", rtn, error.c_str()); } //发送文件内容 rtn = send(s, buf, length, 0); if (rtn == SOCKET_ERROR) {//显示处理结果 cout << "Failed to send!" << endl; } if (rtn > 0) { printf("\nSend %d bytes to client: \n%s\n", rtn, buf); } return; }
最终优化效果如下:
-
404:路径无效,找不到文件
127.0.0.1:5050/invalid.png
-
403:路径有效,但请求的是非法文件
127.0.0.1:5050/image/private.png
-
400:请求的文件不是html、image、video中的一个(此处设定的是400优先级高于404)
127.0.0.1:5050/file/web.css
2.7 异常处理
我们还希望服务器有一定的异常处理能力,我此处关注到的是当前代码中的warning部分:
因此添加一个分支以避免溢出:
//获取文件大小
fseek(fp, 0, SEEK_END);
long long length = ftell(fp);
fseek(fp, 0, SEEK_SET);
if (length >= 4294967295) {//文件过大,告知浏览器申请的文件过大,无法发送,避免溢出
cout << "The file which has been requested is too big!" << endl;
string content("The file which has been requested is too big!");
resp.append(FORBIDDEN);
resp.append("Server: VerySimpleServer\r\n");
resp.append("Content-Type: text/html; charset=ISO-8859-1\r\n");
resp.append("Content-Length: ").append(to_string(content.length())).append("\r\n");
resp.append("\r\n");
resp.append(content);
int rtn = send(s, resp.c_str(), resp.length(), 0);
if (rtn > 0) {
printf("\nSend %d bytes to client: \n%s\n", rtn, resp.c_str());
}
message.clear();//注意返回前一定要清空message,以便下次使用
return;
}
//将文件内容存储至缓冲区
char* buf = (char*)malloc(length + 1);
memset(buf, '\0', length + 1);
fread(buf, length, 1, fp);
fclose(fp);
2.8 服务器完善
最终对我们的服务器进行一定的完善,为各个提示添加颜色,使其更加直观,最终得到的服务器代码如下:
#define _CRT_SECURE_NO_WARNINGS
#pragma once
#include "winsock2.h"
#include <windows.h>
#include <Ws2tcpip.h>
#include <stdio.h>
#include <iostream>
#include <string>
#include <list>
#include <vector>
#include <fstream>
using namespace std;
#pragma comment(lib,"ws2_32.lib")
list<SOCKET> sessionSockets;//会话Socket列表
vector<string> message;//响应报文按照/r/n分割后存储在message中
void color(int x);//设置字体颜色
void split(string recvData);//划分请求报文
void send_reponse(string recvData, string catalogue,SOCKET s);//创建并发送响应报文
void error_tips(SOCKET s, int problem);//错误提示
void main(){
system("color F0");
WSADATA wsaData;
fd_set rfds;//用于检查socket是否有数据到来的的文件描述符
fd_set wfds;//用于检查socket是否可以发送的文件描述符
/* 底层初始化 */
int nRc = WSAStartup(0x0202, &wsaData);
if (nRc) {
color(252); printf("Winsock startup failed with error!\n");
WSAGetLastError();
}
if (wsaData.wVersion != 0x0202) {
color(252); printf("Winsock version is not correct!\n");
WSAGetLastError();
}
color(242); printf("Winsock startup Ok!\n");
//读取配置文件,对监听地址、端口、主目录进行配置
string IP;//服务器IP地址
string catalogue;//主目录
short port = 0;//端口
ifstream config;
config.open("E:/Lab1_Server/config.txt", ios::in);
if (!config.is_open()) {//打开配置文件失败
color(252); cout << "Open config file failed! " << endl;
return;
}
else {
color(242); cout << "Open config file success! "<<endl;
char buf[1024];
memset(buf, '\0', 1024);
while(config.getline(buf, sizeof(buf))){
string s(buf);
if (s.find("IP") != string::npos) {//IP配置行
IP.assign(s,3);
}
else if (s.find("port") != string::npos) {//端口port配置行
port = short(atoi(s.substr(5).c_str()));
}
else if (s.find("catalogue") != string::npos) {//主目录配置行
catalogue.assign(s, 10);
}
}
}
color(242); cout << "Analyse config file success!" << endl;
/* Socket初始化 */
SOCKET srvSocket;
sockaddr_in addr, clientAddr;
SOCKET sessionSocket;
int addrLen;
//创建监听Socket
srvSocket = socket(AF_INET, SOCK_STREAM, 0);
if (srvSocket != INVALID_SOCKET) {
color(242); printf("Socket create Ok!\n");
}
else {
color(252); cout << "Server Socket create failed with error!" << endl;
WSAGetLastError();
}
//设置服务器的端口和地址
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
inet_pton(AF_INET, IP.c_str(), &addr.sin_addr.s_addr);
addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
color(241); cout << "Set Server IP success:" << IP << endl;
color(241); cout << "Set Server port success:" << port << endl;
//将监听Socket与主机地址绑定
int rtn = bind(srvSocket, (LPSOCKADDR)&addr, sizeof(addr));
if (rtn != SOCKET_ERROR) {
color(242); printf("Socket bind Ok!\n");
}
else {
color(252); cout << "Socket bind failed with error!" << endl;
WSAGetLastError();
}
/* 监听 */
rtn = listen(srvSocket, 5);
if (rtn != SOCKET_ERROR) {
color(242); printf("Socket listen Ok!\n");
}
else {
color(252); cout << "Socket listen failed with error!" << endl;
WSAGetLastError();
}
clientAddr.sin_family = AF_INET;
addrLen = sizeof(clientAddr);
char recvBuf[4096];
u_long blockMode = 1;
if ((rtn = ioctlsocket(srvSocket, FIONBIO, &blockMode) == SOCKET_ERROR)) {
color(252); cout << "ioctlsocket() failed with error!\n";
WSAGetLastError();
return;
}
color(242); cout << "ioctlsocket() for server socket ok! Waiting for client connection and data\n\n";
/* 监听Client数据 */
while (true) {
//清空rfds和wfds数组,并等待客户请求
FD_ZERO(&rfds);
FD_ZERO(&wfds);
FD_SET(srvSocket, &rfds);
for (list<SOCKET>::iterator itor = sessionSockets.begin(); itor != sessionSockets.end(); itor++) {
//设置等待所有会话SOKCET可接受数据或可发送数据
FD_SET(*itor, &rfds);
FD_SET(*itor, &wfds);
}
//返回总共可以读或写的句柄个数
int nTotal = select(0, &rfds, &wfds, NULL, NULL);
//如果srvSock收到连接请求,接受客户连接请求
if (FD_ISSET(srvSocket, &rfds)) {
nTotal--;
sessionSocket = accept(srvSocket, (LPSOCKADDR)&clientAddr, &addrLen);
if (sessionSocket != INVALID_SOCKET) {
color(242); printf("Socket listen one client request!\n");
char str[4096];
color(241); cout << "Client IP:" << inet_ntop(AF_INET,&clientAddr.sin_addr,str,sizeof(str)) << endl;
color(241); cout << "Client port:" << htons(clientAddr.sin_port) << endl;
}
else {//与客户端连接失败
color(252); cout << "Failed to connect the client!" << endl;
WSAGetLastError();
}
if ((rtn = ioctlsocket(sessionSocket, FIONBIO, &blockMode) == SOCKET_ERROR)) {
color(252); cout << "ioctlsocket() failed with error!\n";
WSAGetLastError();
return;
}
color(242); cout << "ioctlsocket() for session socket ok! Waiting for client connection and data\n\n";
//设置等待会话SOKCET可接受数据或可发送数据
FD_SET(sessionSocket, &rfds);
FD_SET(sessionSocket, &wfds);
//把新的会话socket加入会话socket列表
sessionSockets.push_back(sessionSocket);
}
//检查会话SOCKET是否有数据到来
if (nTotal > 0) {
for (list<SOCKET>::iterator itor = sessionSockets.begin(); itor != sessionSockets.end(); itor++) {
if (FD_ISSET(*itor, &rfds)) { //如果遍历到的当前socket有数据到来
//receiving data from client
memset(recvBuf, '\0', 4096);
rtn = recv(*itor, recvBuf, 4096, 0);
if (rtn > 0) {
color(242); printf("\nReceived %d bytes from client: \n", rtn);
color(244); cout << recvBuf << endl;
//向服务器发送响应
if (FD_ISSET(*itor, &wfds)) {//遍如果历到的当前socket可发送数据
string recvData(recvBuf);//请求报文
send_reponse(recvData,catalogue,*itor);//构建响应报文并发送
}
}
}
}
}
}
}
void color(int x){
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), x);//设置不同数字的颜色
}
void split(string recvData) {//切割响应报文,按照\r\n分割
string seg("\r\n");//分隔符
int pos = 0;
int size = recvData.size();
for (int i = 0; i < size; ++i) {
pos = recvData.find(seg, i);
if (pos != -1) {
string s = recvData.substr(i, pos - i);
if (s.size() > 0) message.push_back(s);
i = pos + seg.size() - 1;
}
else break;
}
}
void send_reponse(string recvData,string catalogue,SOCKET s) {//创建响应报文,并发送
/*
recvData: 请求报文
catalogue: 主目录
return: 响应报文
*/
string resp;//响应报文
//初始化状态行
const string OK("HTTP/1.1 200 OK\r\n");
const string NOT_FOUND("HTTP/1.1 404 NOT FOUND\r\n");
const string FORBIDDEN("HTTP/1.1 403 FORBIDDEN\r\n");
const string MOVED("HTTP/1.1 301 Moved Permanently\r\n");
const string BAD_REQUEST("HTTP/1.1 400 Bad Request\r\n");
const string WRONG_VERSION("HTTP/1.1 505 HTTP Version Not Supported\r\n");
split(recvData);//将请求报文的每一行分割存储到message中
string route;//请求报文请求的文件路径
bool forbidden = false;//请求的文件是否为私有的不可访问的文件
//根据请求行判断浏览器请求的是什么类型的文件
bool html = false;
bool image = false;
bool video = false;
//解析请求行,即message[0]
if (message[0].find("/ HTTP/1.1") != -1) {//未指明路径,发送默认网页
route = route + catalogue + "/file/web.html";
html = true;
}
else if (message[0].find("HTTP/1.1") == -1) {//HTTP版本不对,我只支持HTTP1.1
resp.append(WRONG_VERSION);
int rtn = send(s, resp.c_str(), resp.length(), 0);
if (rtn == SOCKET_ERROR) {//显示处理结果
color(252); cout << "Failed to send!" << endl;
}
color(252); cout << "505 HTTP Version Not Supported! "<< endl;
if (rtn > 0) {
color(242); printf("\nSend %d bytes to client: \n", rtn);
color(244); cout << resp << endl;
}
message.clear();//注意返回前一定要清空message,以便下次使用
return;
}
else {//分析出相对路径,结合主目录形成绝对路径
int pos_begin = message[0].find("/");
int pos_end = message[0].find(" HTTP/1.1");
route = message[0].substr(pos_begin, pos_end - pos_begin);
if (route.compare("/favicon.ico") == 0) {//单独处理网页图标
route = "/image" + route;
}
route = catalogue + route;
color(242); cout << "Get the request route:" ;
color(244); cout << route << endl;
if (route.find("html") != -1) {//根据请求的文件名判断浏览器想要什么类型的文件
html = true;
}
else if (route.find("jpg") != -1 ||
route.find("png") != -1 ||
route.find("webp") != -1 ||
route.find("ico") != -1) {
image = true;
}
else if (route.find("mp4")!=-1) {
video = true;
}
}
bool understand = html | image | video;//判断是否读懂了请求报文
if (understand) {//正确解析出了报文中的路径
//根据route打开文件:
FILE* fp = fopen(route.c_str(), "rb");
if (fp == NULL) {//未找到文件
error_tips(s, 404);//反馈错误提示,服务器输出404错误
}
else {//找到了文件
if (route.find("private") != -1) {//请求的是私人的不可发送的文件,403
error_tips(s, 403);
}
else {//判断了这么多次终于可以发送了
//获取文件大小
fseek(fp, 0, SEEK_END);
long long length = ftell(fp);
fseek(fp, 0, SEEK_SET);
if (length >= 4294967295) {//文件过大,告知浏览器申请的文件过大,无法发送,避免溢出
color(252); cout << "The file which has been requested is too big!" << endl;
string content("The file which has been requested is too big!");
resp.append(FORBIDDEN);
resp.append("Server: VerySimpleServer\r\n");
resp.append("Content-Type: text/html; charset=ISO-8859-1\r\n");
resp.append("Content-Length: ").append(to_string(content.length())).append("\r\n");
resp.append("\r\n");
resp.append(content);
int rtn = send(s, resp.c_str(), resp.length(), 0);
if (rtn > 0) {
color(242); printf("\nSend %d bytes to client: \n", rtn);
color(244); cout << resp << endl;
}
message.clear();//注意返回前一定要清空message,以便下次使用
return;
}
//将文件内容存储至缓冲区
char* buf = (char*)malloc(length + 1);
memset(buf, '\0', length + 1);
fread(buf, length, 1, fp);
fclose(fp);
//构造响应报文
resp.append(OK);
resp.append("Server: VerySimpleServer\r\n");
if (html) resp.append("Content-Type: text/html; charset=ISO-8859-1\r\n");
else if(image) resp.append("Content-Type: image/*\r\n");
else if(video) resp.append("Content-Type: video/mp4\r\n");
resp.append("Content-Length: ").append(to_string(length)).append("\r\n");
resp.append("\r\n");
//发送首部
int rtn = send(s, resp.c_str(), resp.length(), 0);
if (rtn == SOCKET_ERROR) {//显示处理结果
color(252); cout << "Failed to send!" << endl;
}
if (rtn > 0) {
color(242); printf("\nSend %d bytes to client: \n", rtn);
color(244); cout << resp << endl;
}
//发送文件内容
rtn = send(s, buf, length, 0);
if (rtn == SOCKET_ERROR) {//显示处理结果
color(252); cout << "Failed to send!" << endl;
}
color(242); cout << "200 OK, Success to send the file!" << endl;
if (rtn > 0) {
color(242); printf("\nSend %d bytes to client: \n", rtn);
color(244); cout << buf << endl;
}
}
}
}
else {//未读懂报文,我只能发html,image,mp4,你请求的文件不是我能读懂的
error_tips(s, 400);
}
message.clear();//注意返回前一定要清空message,以便下次使用
return;
}
void error_tips(SOCKET s,int problem) {
//错误提示
const string NOT_FOUND("HTTP/1.1 404 NOT FOUND\r\n");
const string FORBIDDEN("HTTP/1.1 403 FORBIDDEN\r\n");
const string BAD_REQUEST("HTTP/1.1 400 Bad Request\r\n");
//发送错误提示图片——即发送一个包含图片的html网页
string error;
string route;
if (problem == 404) {
route = route + "file/404.html";
color(245); cout << "404 NOT FOUND, Can't find the file "<< endl;
error.append(NOT_FOUND);
}
else if (problem == 403) {
route = route + "file/403.html";
color(245); cout << "403 FORBIDDEN, The request is forbidden!" << endl;
error.append(FORBIDDEN);
}
else if (problem == 400) {
route = route + "file/400.html";
color(245); cout << "400 Bad Request, Can't understand the recvData!" << endl;
error.append(BAD_REQUEST);
}
FILE* fp = fopen(route.c_str(), "rb");
//获取文件大小
fseek(fp, 0, SEEK_END);
int length = ftell(fp);
fseek(fp, 0, SEEK_SET);
//将文件内容存储至缓冲区
char* buf = (char*)malloc(length + 1);
memset(buf, '\0', length + 1);
fread(buf, length, 1, fp);
fclose(fp);
error.append("Server: VerySimpleServer\r\n");
error.append("Content-Type: text/html\r\n");
error.append("Content-Length: ").append(to_string(length)).append("\r\n");
error.append("\r\n");
//发送首部
int rtn = send(s, error.c_str(), error.length(), 0);
if (rtn == SOCKET_ERROR) {//显示处理结果
color(252); cout << "Failed to send!" << endl;
}
if (rtn > 0) {
color(242); printf("\nSend %d bytes to client: \n", rtn);
color(244); cout << error << endl;
}
//发送文件内容
rtn = send(s, buf, length, 0);
if (rtn == SOCKET_ERROR) {//显示处理结果
color(252); cout << "Failed to send!" << endl;
}
if (rtn > 0) {
color(242); printf("\nSend %d bytes to client: \n", rtn);
color(244); cout << buf << endl;
}
return;
}