我们知道浏览器是http(s)的客户端,目的是连接远程的http服务器,然后服务器返回浏览器数据。浏览器接收数据解析数据之后展现出来。
我们看到的外在表现就是,浏览器访问一个url,然后就得到相应的web页面。在此期间,浏览器与http服务器是通过http协议传输数据。传输层是tcp协议,因为他是有连接,可靠的协议。
工欲善其事必先利其器!!!
这里我使用了阿里云轻量化应用服务器。有条件还可以买个域名。
下载安装好以后,打开Visual Studio Installer安装关于Linux的插件。
安装好以后,创建项目。
选择《工具》---->选择《选项》-------->选择《跨平台》----->选择《添加》 ,然后输入:
右键项目,选择《属性》----->选择《C/C++》,设置c或者c++的编译器。
c用gcc,c++用g++,若编译过程发现找不到,可以用目录代替。比如我的c编译器就使用了目录。
点击确定,我们就可以将本地代码复制到Linux服务器下的一个projects的文件夹下(在根目录)
Clion的配置较为复杂(需要在服务器上安装CMake,gdb等等),但我认为却是相对好用的一个。
右边的是服务器内部文件,下边是服务器远程终端,上面是直接从服务器里打开的文件。
在敲代码前,我们应该大致了解一下http服务器的工作过程。
浏览器与Web服务器的HTTP端口(默认为80)建立一个TCP套接字连接。例如,http://www.raying.top
通过TCP套接字,客户端向Web服务器发送一个文本的请求报文。
Web服务器解析请求,定位请求资源。服务器将资源复本写到TCP套接字,由客户端读取。
若connection 模式为close,则服务器主动关闭TCP连接,客户端被动关闭连接,释放TCP连接;若connection 模式为keepalive,则该连接会保持一段时间,在该时间内可以继续接收请求。
客户端浏览器首先解析状态行,查看表明请求是否成功的状态代码。然后解析每一个响应头,响应头告知以下为若干字节的HTML文档和文档的字符集。客户端浏览器读取响应数据HTML,根据HTML的语法对其进行格式化,并在浏览器窗口中显示。
打开浏览器通过抓包分析,即可对请求头和响应头有大致了解。
3.1.1 这里可以按行读取请求头部,一个字符一个字符的读取客户端数据:
3.1.2 这里的 read() 函数 是头文件
//返回值: -1 表示读取出错, 等于0表示读到一个空行, 大于0 表示成功读取一行
int get_line(int sock, char *buf, int size) {
int count = 0; // 已经读到的字符数
char ch = '\0'; // 读到的字符
int len = 0; // 已经读到的的长度
while ((count < size - 1) && ch != '\n') {
len = read(sock, &ch, 1); // 读取客户端发送的数据,1个字符一个字符读
if (len == 1) { // 成功读到一个字符
if (ch == '\r') { // 回车符号
continue;
} else if (ch == '\n') { // 换行符
break;
}
buf[count] = ch; // 处理正常的字符,非回车换行符
count++;
} else if (len == -1) { //读取出错
perror("read failed");
count = -1; // 返回-1表示读取出错
break;
} else { // read 返回0,客户端关闭sock 连接.
fprintf(stderr, "client close.\n");
count = -1;
break;
}
}
if (count >= 0) buf[count] = '\0';
return count;
}
获取到http请求后,我们就可以对其按照http协议规定的那样进行解析。下图是解析请求时候的思路图,可以通过这个思路去设计代码。
void *do_http_request(void *pclient_sock) {
int len = 0;
char buf[256];
char method[64];
char url[256];
char path[256];
int client_sock = *(int *) pclient_sock;
struct stat st;
/*读取客户端发送的http 请求*/
//1.读取请求行
len = get_line(client_sock, buf, sizeof(buf));
if (len > 0) {//读到了请求行
int i = 0, j = 0;
while (!isspace(buf[j]) && (i < sizeof(method) - 1)) {
method[i] = buf[j];
i++;
j++;
}
method[i] = '\0';
if (debug) printf("request method: %s\n", method);
if (strncasecmp(method, "GET", i) == 0) { //只处理get请求
if (debug) printf("method = GET\n");
//获取url
while (isspace(buf[j++]));//跳过白空格
i = 0;
while (!isspace(buf[j]) && (i < sizeof(url) - 1)) {
url[i] = buf[j];
i++;
j++;
}
url[i] = '\0';
if (debug) printf("url: %s\n", url);
//继续读取http 头部
do {
len = get_line(client_sock, buf, sizeof(buf));
if (debug) printf("read: %s\n", buf);
} while (len > 0);
//***定位服务器本地的html文件***
//处理url 中的?
{
char *pos = strchr(url, '?'); // 查找字符串中有无?
if (pos) {
*pos = '\0';
printf("real url: %s\n", url);
}
}
// sprintf(path, "./html_docs/%s", url);
sprintf(path, "./resource/%s", url);
if (debug) printf("path: %s\n", path);
//执行http 响应
//判断文件是否存在,如果存在就响应200 OK,同时发送相应的html 文件,如果不存在,就响应 404 NOT FOUND.
if (stat(path, &st) == -1) {//文件不存在或是出错
fprintf(stderr, "stat %s failed. reason: %s\n", path, strerror(errno));
not_found(client_sock);
} else {//文件存在
if (S_ISDIR(st.st_mode)) { // 判断路径是不是目录
strcat(path, "/index.html"); // 追加字符串index.html到结尾
}
do_http_response(client_sock, path);
}
} else {//非get请求, 读取http 头部,并响应客户端 501 Method Not Implemented
fprintf(stderr, "warning! other request [%s]\n", method);
do {
len = get_line(client_sock, buf, sizeof(buf));
if (debug) printf("read: %s\n", buf);
} while (len > 0);
unimplemented(client_sock); //请求未实现
}
} else {//请求格式有问题,出错处理
bad_request(client_sock); //在响应时再实现
}
close(client_sock);
if (pclient_sock) free(pclient_sock);//释放动态分配的内存
return NULL;
}
值得注意的是,这里加入了多线程的用法,后文详细讲解!
在我们放服务器拿到解析后的http请求后,就可以向浏览器做出响应了。
下图是响应http请求的一个思路:
void do_http_response(int client_sock, const char *path) {
int ret = 0;
FILE *resource = NULL;
resource = fopen(path, "r");
if (resource == NULL) {
not_found(client_sock);
return;
}
//1.发送http 头部
ret = headers(client_sock, resource);
//2.发送http body .
if (!ret) {
cat(client_sock, resource);
}
fclose(resource);
}
3.4.1 在发送http头部时候,需要传入两个参数:
3.4.2 这里要用到一个 stat() 函数 用来返回文件的状态信息,需要调用3个头文件
#include
int stat(const char *path, struct stat *buf);
参数:
path:
文件的路径
buf:
传入的保存文件状态的指针,用于保存文件的状态
返回值:
成功返回0,失败返回-1,设置errno
3.4.3 通过 strcat() 函数 将服务器信息追加到buf里面
3.4.4 将buf信息传递给客户端socket
/****************************
*返回关于响应文件信息的http 头部
*输入:
* client_sock - 客服端socket 句柄
* resource - 文件的句柄
*返回值: 成功返回0 ,失败返回-1
******************************/
int headers(int client_sock, FILE *resource) {
struct stat st;
int fileid = 0; //文件传输错误代码
char tmp[64];
char buf[1024] = {0};
strcpy(buf, "HTTP/1.0 200 OK\r\n"); //将src指针指向的字符串复制(替换)到buf指向的数组中
strcat(buf, "Server: Ray Server\r\n"); //将src指针指向的字符串添加到dst指针指向的字符串后面
strcat(buf, "Content-Type: text/html\r\n");
strcat(buf, "Connection: Close\r\n");
fileid = fileno(resource);
if (fstat(fileid, &st) == -1) { // 服务器内部出错了
inner_error(client_sock);
return -1;
}
snprintf(tmp, 64, "Content-Length: %ld\r\n\r\n", st.st_size);
strcat(buf, tmp);
if (debug) fprintf(stdout, "header: %s\n", buf);
// 将文件内容发送给客户端socket,0是一个flag
if (send(client_sock, buf, strlen(buf), 0) < 0) {
fprintf(stderr, "send failed. data: %s, reason: %s\n", buf, strerror(errno));
return -1;
}
return 0;
}
除了传入和发送http头部传入的参数外,还需要用到以下三个函数:
/****************************
*说明:实现将html文件的内容按行
读取并送给客户端
****************************/
void cat(int client_sock, FILE *resource) {
char buf[1024];
// 先读取一行并保存
// 从 resource 流中读取 size 个字符存储到字符指针变量 buf 所指向的内存空间
fgets(buf, sizeof(buf), resource);
// feof()是检测流上的文件结束符的函数,如果文件结束,则返回非0值,否则返回0
while (!feof(resource)) {
int len = write(client_sock, buf, strlen(buf));
if (len < 0) {//发送body 的过程中出现问题,怎么办?1.重试? 2.break
fprintf(stderr, "send body error. reason: %s\n", strerror(errno));
break;
}
if (debug) fprintf(stdout, "%s", buf);
fgets(buf, sizeof(buf), resource);
}
}
为了增强代码的健壮性,我们必须对一些容易产生的错误进行错误处理。
这里为了方便看到效果,没有编写专门的错误页面。直接将html代码发送给客户端。
3.6.1 500(服务器内部错误) 服务器遇到错误,无法完成请求。
void unimplemented(int client_sock) {
const char *reply = "HTTP/1.0 501 Method Not Implemented\r\n\
Content-Type: text/html\r\n\
\r\n\
\r\n\
\r\n\
Method Not Implemented \r\n\
\r\n\
\r\n\
HTTP request method not supported.\r\n\
\r\n\
";
int len = write(client_sock, reply, strlen(reply));
if (debug) fprintf(stdout, reply);
if (len <= 0) {
fprintf(stderr, "send reply failed. reason: %s\n", strerror(errno));
}
}
3.6.2 400(错误请求) 服务器不理解请求的语法。
void bad_request(client_sock) {
const char *reply = "HTTP/1.0 400 BAD REQUEST\r\n\
Content-Type: text/html\r\n\
\r\n\
\r\n\
\r\n\
BAD REQUEST \r\n\
\r\n\
\r\n\
Your browser sent a bad request!\r\n\
\r\n\
";
int len = write(client_sock, reply, strlen(reply));
if (len <= 0) {
fprintf(stderr, "send reply failed. reason: %s\n", strerror(errno));
}
}
3.6.3 服务器内部出错
void inner_error(int client_sock) {
const char *reply = "HTTP/1.0 500 Internal Sever Error\r\n\
Content-Type: text/html\r\n\
\r\n\
\r\n\
\r\n\
\r\n\
Inner Error \r\n\
\r\n\
\r\n\
服务器内部出错.\r\n\
\r\n\
";
int len = write(client_sock, reply, strlen(reply));
if (debug) fprintf(stdout, reply);
if (len <= 0) {
fprintf(stderr, "send reply failed. reason: %s\n", strerror(errno));
}
}
3.6.4 404(未找到) 服务器找不到请求的网页。
void not_found(int client_sock) {
const char *reply = "HTTP/1.0 404 NOT FOUND\r\n\
Content-Type: text/html\r\n\
\r\n\
\r\n\
\r\n\
\r\n\
NOT FOUND \r\n\
\r\n\
\r\n\
文件不存在!\r\n\
The server could not fulfill your request because the resource specified is unavailable or nonexistent.\r\n\
\r\n\
";
int len = write(client_sock, reply, strlen(reply));
if (debug) fprintf(stdout, reply);
if (len <= 0) {
fprintf(stderr, "send reply failed. reason: %s\n", strerror(errno));
}
}
int main(void) {
int sock;
struct sockaddr_in server_addr;
sock = socket(AF_INET, SOCK_STREAM, 0);
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET; //选择协议IPV4
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//监听本地所有IP地址
server_addr.sin_port = htons(SERVER_PORT);//绑定端口号
bind(sock, (struct sockaddr *) &server_addr, sizeof(server_addr));
listen(sock, 128);
printf("等待客户端的连接\n");
int done = 1;
while (done) {
struct sockaddr_in client;
int client_sock, len, i;
char client_ip[64];
char buf[256];
pthread_t id;
int *pclient_sock = NULL;
socklen_t client_addr_len;
client_addr_len = sizeof(client);
client_sock = accept(sock, (struct sockaddr *) &client, &client_addr_len);
//打印客户端IP地址和端口号
printf("client ip: %s\t port : %d\n",
inet_ntop(AF_INET, &client.sin_addr.s_addr, client_ip, sizeof(client_ip)),
ntohs(client.sin_port));
/*处理http 请求,读取客户端发送的数据*/
//启动线程处理http 请求
pclient_sock = (int *) malloc(sizeof(int));
*pclient_sock = client_sock;
// 多线程
pthread_create(&id, NULL, do_http_request, (void *) pclient_sock);
}
close(sock);
return 0;
}
可以发现,服务器也打印除了很多客户端的相关信息。
OracleRay/MiniHttpServer (github.com)https://github.com/OracleRay/MiniHttpServer