一、HTTP 概要
在所学理论知识的基础上,编写 HTTP(Hypertex Transfer Protocol,超文本传输协议)服务器端,即 Web 服务器端。
1.1 理解 Web 服务器端
简单地给一个 Web 服务器端的定义:基于 HTTP 协议,服务器端将网页对应文件传输给客户端。
HTTP 是可以根据客户端请求而跳转的结构化信息。例如,通过浏览器访问图灵社区的主页时,首页文件将传输到浏览器并展现给大家,此时可以点击鼠标🖱跳转到任意页面。这种可跳转的文本称为超文本。
HTTP 协议又是什么?
HTTP 是以超文本传输为目的而设计的应用层协议,这种协议同样属于 TCP/IP 实现的协议,所以我们可以利用封装好的 socket 来 DIY 一个 HTTP。最后,我们就相当于实现一个 Web 服务器端。另外,浏览器也属于基于套接字的客户端,因为连接到任意 Web 服务器端时,浏览器内部也会创建套接字。只不过浏览器多了一项功能,它将服务器端传输的 HTML 格式的超文本解析为可读性较强的试图。总之,Web 服务器端是以 HTTP 协议为基础传输超文本的服务器端。
1.2 HTTP
下面详细讨论 HTTP 协议,虽然它相对简单,但要完全驾驭也不简单。接下来的内容,是实现 Web 服务器端时的必要内容,关于 HTTP 的其他内容可以再补充。
1.2.1 无状态的 Stateless 协议
为了在网络环境下同时向大量客户端提供服务,HTTP 协议的请求及响应方式设计如下图所示。
从上图可以看到,服务器端响应客户端请求后立即断开连接。即,服务器端不会维持客户端状态。即使同一客户端再次发送请求,服务器端也没有记忆,同样当作新客户端来处理。因此,HTTP 协议也称 “无状态的 Stateless 协议“。
Cookie 和 Session
为了弥补 HTTP 无法保持连接的特点,Web 编程中通常会使用 Cookie 和 Session 技术。在淘宝中,我们将货物放入购物车中,下次我们再登陆时,信息并不会丢失,这就是通过 Cookie 和 Session 技术实现的。
1.2.2 请求消息(Request Message)的结构
下面是客户端向服务器端发送的请求消息的结构,Web 服务器需要解析并响应客户端请求,客户端和服务器端之间的数据请求方式标准如下图所示。
从上图可以看到,请求消息可以分为请求行、消息头、消息体等 3 个部分。其中,请求行含有请求方式(请求目的)信息。典型的请求方式有 GET 和 POST,GET 主要用于请求数据,POST 主要用于传输数据。
请求行
为了降低代码复杂度,现在只响应 GET 请求的 Web 服务器端。下面是针对上图中的请求行信息的解释
GET /index.html HTTP/1.1 // 请求 index.html文件,希望以 1.1 版本的HTTP 协议通信
请求行只能通过 1 行发送,所以服务器端从 HTTP 请求中提取出第一行,并且分析请求行中包含的信息。
消息头
请求行下面的消息头中包含发送请求(将要接收响应信息的)浏览器信息、用户认证信息等关于 HTTP 消息的附加信息。
消息体
最后的消息体中装有客户端向服务器端传输的数据,为了装入数据,需要以 POST 方式发送请求。但是我们的目标是实现 GET 方式的服务器端,所以这部分内容可以忽略。另外,消息体和消息头之间以空行分开,因此不会发生边界问题。
1.2.2 响应消息(Response Message)的结构
下面是 Web 服务器端向客户端传递的响应信息的结构。从下图中可以看到,该响应消息由状态行、头信息、消息体等 3 个部分构成。状态行中含有关于请求的状态信息,这是与其请求消息相比最为显著的区别。
状态行
从上图中可以看到,第一个字符串状态行中含有关于客户端请求的处理结果。例如,客户端请求 index.html 文件时,表示 index.html 文件是否存在、服务器是否发生问题而无法响应等不同情况的信息将写入状态行。上图中的 HTTP/1.1 200 OK
具有如下含义:
HTTP/1.1 200 OK // 我想用 HTTP1.1 版本进行响应,你的请求已正确处理(200 OK)
表示 “客户端请求的执行结果” 的数字称为状态码,典型的有以下几种。
- 200 OK:成功处理了请求!
- 404 Not Found:请求的文件不存在!
- 400 Bad Request:请求方式错误,请检查!
消息头
消息头中含有传输的数据类型和长度等信息,上图中的消息头含有如下信息:
Server: SimpleWebServer // 服务器端名为 SimpleWebServer
Content-type: text/html // 传输的数据类型 text/html,html 格式的文本数据
Content-length: 2048 // 数据长度不超过 2048 字节
消息体
最后插入 1 个空行后,通过消息头发送客户端请求的文件数据。
<html>
<body>
...
</body>
</html>
当然,了解上述的知识后就可以实现一个简单的 Web 服务器了,要想实现完整的 Web 服务器还需要更多 HTTP 协议相关知识。
1.3 实现简单的 Web 服务器端
现在开始在 HTTP 协议的基础上编写 Web 服务器端。先给出 Windows 平台下的示例,再给出 Linux 下的示例。前面介绍了 HTTP 协议的相关背景知识,有了这些基础就不难分析源代码。
1.3.1 实现基于 Windows 的多线程 Web 服务器端
Web 服务器端采用 HTTP 协议,即使用 IOCP 或 epoll 模型也不会大幅提升性能。客户端和服务器端交换 1 次数据后将立即断开连接,没有足够时间发挥 IOCP 或 epoll 的优势。在服务器端和客户端保持长连接的前提下频繁发送不小大小不一的消息时(最典型的就是网游服务端),才能真正发挥出这 2 种模型的优势。
能否通过 Web 服务器端比较 IOCP 和 epoll 的性能?
Windows下高并发的高性能服务器一般会采用完成端口IOCP技术,Linux下则会采用Epoll。
利用 IOCP 和 epoll 实现 Web 服务器端本身没有问题,但无法通过这种服务器端完全体会到 IOCP 和 epoll 的优点。
通过多线程模型实现 Web 服务器端。也就是说,客户端每次请求时,都创建 1个新线程响应客户端请求。
// webserv_win.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#include <process.h>
#define BUF_SIZE 2048
#define BUF_SMALL 100
unsigned WINAPI RequestHandler(void *arg);
char* ContentType(char *file);
void SendData(SOCKET sock, char *ct, char *filename);
void SendErrorMSG(SOCKET sock);
void ErrorHandling(char *message);
int main(int argc, char *argv[]) {
// 1.检查输入
if (argc != 2) {
printf("Usage: %s <port>\n", argv[0]);
exit(1);
}
// 2.规定 socket 版本
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
ErrorHandling("WSAStartup() error!");
// 3.初始化服务器端套接字
SOCKET hServSock;
SOCKADDR_IN servAdr;
hServSock = socket(PF_INET, SOCK_STREAM, 0);
memset(&servAdr, 0, sizeof(servAdr));
servAdr.sin_family = AF_INET;
servAdr.sin_addr.s_addr = htonl(INADDR_ANY);
servAdr.sin_port = htons(atoi(argv[1]));
// 4.绑定套接字 bind
if (bind(hServSock, (SOCKADDR*)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)
ErrorHandling("bind() error!");
// 5.进入 listen 监听状态
if (listen(hServSock, 5) == SOCKET_ERROR)
ErrorHandling("listen() error");
// 6.请求及响应
SOCKADDR_IN clntAdr;
int clntAdrSize;
SOCKET hClntSock;
HANDLE hThread;
DWORD dwThreadID;
while (true) {
clntAdrSize = sizeof(clntAdr);
// 7.与客户端建立连接
hClntSock = accept(hServSock, (SOCKADDR*)&clntAdr, &clntAdrSize);
printf("Connection Request : %s:%d\n",
inet_ntoa(clntAdr.sin_addr),
ntohs(clntAdr.sin_port));
// 8.建立线程与客户端建立业务联系
hThread = (HANDLE)_beginthreadex(NULL, 0,
RequestHandler, (void*)hClntSock,
0, (unsigned*)&dwThreadID);
}
// printf("hello");
closesocket(hServSock);
WSACleanup();
return 0;
}
unsigned WINAPI RequestHandler(void *arg) {
// printf("nn");
SOCKET hClntSock = (SOCKET) arg;
char buf[BUF_SIZE];
char method[BUF_SMALL];
char ct[BUF_SMALL];
char fileName[BUF_SMALL];
recv(hClntSock, buf, BUF_SIZE, 0);
if (strstr(buf, "HTTP/") == NULL) { // 查看是否为 HTTP 提出的请求
SendErrorMSG(hClntSock);
closesocket(hClntSock);
return 1;
}
strcpy(method, strtok(buf, ' /'));
if (strcmp(method, "GET")) // 查看是否为 GET 方式的请求
SendErrorMSG(hClntSock);
strcpy(fileName, strtok(NULL, " /")); // 查看请求文件名
printf("%s", fileName);
strcpy(ct, ContentType(fileName)); // 查看 Content-type
SendData(hClntSock, ct, fileName); // 响应
return 0;
}
void SendData(SOCKET sock, char *ct, char *fileName) {
char protocol[] = "HTTP/1.0 200 OK\r\n";
char servName[] = "Server:simple web server\r\n";
char cntLen[] = "Content-lenght:2048\r\n";
char cntType[BUF_SMALL];
char buf[BUF_SIZE];
FILE *sendFile;
sprintf(cntType, "Content-type:%s\r\n\r\n", ct);
if ((sendFile=fopen(fileName, "r")) == NULL) {
SendErrorMSG(sock);
return;
}
// 传输头信息
send(sock, protocol, strlen(protocol), 0);
send(sock, servName, strlen(servName), 0);
send(sock, cntLen, strlen(cntLen), 0);
send(sock, cntType, strlen(cntType), 0);
// 传输请求数据
while (fgets(buf, BUF_SIZE, sendFile) != NULL)
send(sock, buf, strlen(buf), 0);
closesocket(sock); // 由 HTTP 协议响应后断开
}
void SendErrorMSG(SOCKET sock) { // 发生错误时传递消息
char protocol[] = "HTTP/1.0 400 Bad Request\r\n";
char servName[] = "Server:simple web server\r\n";
char cntLen[] = "Content-length:2048\r\n";
char cntType[] = "Content-type:text/html\r\n\r\n";
char content[] = "<html><head><title>NETWORK</title></head>"
"<body><font size=+5><br> 发送错误!查看请求文件名和请求方式!"
"</font></body></html>";
send(sock, protocol, strlen(protocol), 0);
send(sock, servName, strlen(servName), 0);
send(sock, cntLen, strlen(cntLen), 0);
send(sock, cntType, strlen(cntType), 0);
send(sock, content, strlen(content), 0);
closesocket(sock);
}
char* ContentType(char *file) { // 区分Content-type
char extension[BUF_SMALL];
char fileName[BUF_SMALL];
strcpy(fileName, file);
strtok(fileName, ".");
strcpy(extension, strtok(NULL, "."));
if (!strcmp(extension, "html") || !strcmp(extension, "html="))
return "text/html";
else
return "text/plain";
}
void ErrorHandling(char *message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
上述程序出了点小问题,不能够进入到业务处理函数 RequestHandler()
就推出了
1.3.2 实现基于 Linux 的多线程 Web 服务器端
Linux 下的 Web 服务器使用的是标准 I/O 函数。
// webserv_linux.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <pthread.h>
#define BUF_SIZE 1024
#define SMALL_BUF 100
void* request_handler(void *arg);
void send_data(FILE *fp, char *ct, char *file_name);
char* content_type(char *file);
void send_error(FILE *fp);
void error_handling(char *message);
int main(int argc, char *argv[]) {
// 1.判断输入
if (argc != 2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
// 2.初始化套接字
int serv_sock;
struct sockaddr_in serv_adr;
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
// 3.绑定套接字
if (bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
error_handling("bind() error");
// 4.进入listen 监听状态
if (listen(serv_sock, 20) == -1)
error_handling("listen() error");
int clnt_sock;
struct sockaddr_in clnt_adr;
int clnt_adr_size;
char buf[BUF_SIZE];
pthread_t t_id;
while (1) {
clnt_adr_size = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_size);
printf("Connection Request : %s:%d\n",
inet_ntoa(clnt_adr.sin_addr),
ntohs(clnt_adr.sin_port));
pthread_create(&t_id, NULL, request_handler, &clnt_sock);
pthread_detach(t_id);
}
close(serv_sock);
return 0;
}
void* request_handler(void *arg) {
int clnt_sock = *((int*)arg);
char req_line[SMALL_BUF];
FILE *clnt_read;
FILE *clnt_write;
char method[10];
char ct[15];
char file_name[30];
clnt_read = fdopen(clnt_sock, "r");
clnt_write = fdopen(dup(clnt_sock), "w");
fgets(req_line, SMALL_BUF, clnt_read);
if (strstr(req_line, "HTTP/") == NULL) {
send_error(clnt_write);
fclose(clnt_read);
fclose(clnt_write);
return;
}
strcpy(method, strtok(req_line, " /"));
strcpy(file_name, strtok(NULL, " /"));
strcpy(ct, content_type(file_name));
if (strcmp(method, "GET") != 0) {
send_error(clnt_write);
fclose(clnt_read);
fclose(clnt_write);
return;
}
fclose(clnt_read);
send_data(clnt_write, ct, file_name);
}
void send_data(FILE *fp, char *ct, char *file_name) {
char protocol[] = "HTTP/1.0 200 OK \r\n";
char server[] = "Server:Linux Web Server \r\n";
char cnt_len[] = "Content-length:2048\r\n";
char cnt_type[SMALL_BUF];
char buf[BUF_SIZE];
FILE *send_file;
sprintf(cnt_type, "Content_type:%s\r\n\r\n", ct);
send_file = fopen(file_name, "r");
if (send_file == NULL) {
send_error(fp);
return;
}
// 传输头信息
fputs(protocol, fp);
fputs(server, fp);
fputs(cnt_len, fp);
fputs(cnt_type, fp);
// 传输请求数据
while (fgets(buf, BUF_SIZE, send_file) != NULL) {
fputs(buf, fp);
fflush(fp);
}
fflush(fp);
fclose(fp);
}
char* content_type(char *file) {
char extension[SMALL_BUF];
char file_name[SMALL_BUF];
strcpy(file_name, file);
strtok(file_name, ".");
strcpy(extension, strtok(NULL, "."));
if (!strcmp(extension, "html") || !strcmp(extension, "htm"))
return "text/html";
else
return "text/plain";
}
void send_error(FILE *fp) {
char protocol[] = "HTTP/1.0 400 Bad Request\r\n";
char server[] = "Server:Linux Web Server \r\n";
char cnt_len[] = "Content-length:2048\r\n";
char cnt_type[] = "Content-type:text/html\r\n\r\n";
char content[] = "<html><head><title>NETWORK</title></head>"
"<body><font size=+5><br>发生错误!请查看文件名和请求方式!"
"</font></body></html>";
fputs(protocol, fp);
fputs(server, fp);
fputs(cnt_len, fp);
fputs(cnt_type, fp);
fflush(fp);
}
void error_handling(char *message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}