我们知道HTTP依赖于面向连接的TCP进行消息传递。所以我们实际上是要构建一个能够接受TCP连接并通过TCP发送HTTP报文给用户浏览器的服务器。

如何构建一个TCP服务器?

​《UNIX网络编程》​​ 第四章 基本TCP套接字编程

作为服务器,我们要做的就是:1.接受TCP连接请求,2.接收客户发的数据(即HTTP请求报文),3.并回传数据(即HTTP响应报文),4.随后断开该连接。

一个典型的TCP客户端和服务器交互过程以及用到的函数:

逐步构建HTTP服务器(一)——构建一个能返回HTTP报文的服务器_响应报文

  1. 接受TCP连接请求
    // 使用socket()获得一个TCP套接字 // AF_INET IPv4 // SOCK_STREAM 字节流套接字 // IPPROTO_TCP TCP传输协议 int listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // 准备一个网际网套接字地址,该地址主要包括协议族、主机地址和端口 // 我们准备接受所有主机的连接,所以赋的是通配地址(INADDR_ANY) // 端口是8080,之后我们将通过这个端口访问我们的服务器 sockaddr_in servaddr; bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(8080); // 将TCP套接字和网际网套接字地址绑定在一起 bind(listenfd, (sockaddr *) &servaddr, sizeof(servaddr)); // listen()将该套接字转换成一个监听套接字 // LISTENQ为连接完成+正在连接的连接数的最大值 listen(listenfd, LISTENQ); while (1) { // accept()用于接受连接,并返回一个文件描述符代表与所返回的客户的TCP连接,之后与客户的通信(read/write/close)都将通过这个描述符进行 // accept()会一直阻塞在这里,直至有新的连接建立 // cliaddr和addrlen用来返回已连接的对端进程(客户)的协议地址 sockaddr_in cliaddr; socklen_t clilen; // socklen_t aka unsigned int connfd = accept(listenfd, (sockaddr *) &cliaddr, &clilen);
  2. 接收客户发的数据(即HTTP请求报文)
    char buf[MAXLINE]; // read()用来接收数据,并返回收到数据的字节数 // 若用户没有发送数据,将一直阻塞在这里 int n = read(connfd, buf, MAXLINE);
    在这里为了简单,我们不对HTTP请求报文做解析,也就是说对任何的HTTP请求都返回相同数据。
  3. 并回传数据(即HTTP响应报文)
    // write()用来向客户发送数据 // write()也会阻塞在此直至数据发送完成 write(connfd, package.c_str(), package.size());
    这里package是c++的string对象。
    我们需要先了解一下HTTP响应报文:
    逐步构建HTTP服务器(一)——构建一个能返回HTTP报文的服务器_html_02
    我们根据格式写了一个响应报文:
    std::string header = "HTTP/1.1 200 OK\r\nServer: myhttpd\r\nContent-Type: text/html\r\n"; std::string body = "<!DOCTYPE html>\r\n<html>\r\n <head>\r\n <title>Welcome to myhttpd!</title>\r\n <style>\r\n body {\r\n width: 35em;\r\n margin: 0 auto;\r\n font-family: Tahoma, Verdana, Arial, sans-serif;\r\n }\r\n span:nth-child(1) {\r\n color: red;\r\n }\r\n span:nth-child(2) {\r\n color: orange;\r\n }\r\n span:nth-child(3) {\r\n color: yellow;\r\n }\r\n span:nth-child(4) {\r\n color: green;\r\n }\r\n span:nth-child(5) {\r\n color: blue;\r\n }\r\n span:nth-child(6) {\r\n color: blueviolet;\r\n }\r\n span:nth-child(7) {\r\n color: purple;\r\n }\r\n </style>\r\n </head>\r\n <body>\r\n <h1>\r\n Welcome to <span>m</span><span>y</span><span>h</span><span>t</span\r\n ><span>t</span><span>p</span><span>d</span>!\r\n </h1>\r\n <p>\r\n If you see this page, the myhttpd web server is successfully completed.\r\n </p>\r\n\r\n <p>\r\n For more information please refer to\r\n <a href=\"http://github.com/ithepug/myhttpd\">myhttpd</a>.<br />\r\n </p>\r\n\r\n <p><em>Thank you for using myhttpd.</em></p>\r\n </body>\r\n</html>\r\n"; std::string package = header + "\r\n" + body;
  4. 随后断开该连接
    close(connfd); }

需要注意的地方

  1. 1.接受TCP连接请求,2.接收客户发的数据,3.并回传数据,4.随后断开该连接,四个步骤都在一个while(1)中。
    while(1) { accept(); read(); write(); close(); }
    这样我们无意中实现了一个服务器可以连接多个客户端,虽然只是每一时刻与一个客户端连接。
  2. 我们获得的套接字是阻塞的。
    若一个套接字是阻塞的,就意味着对它进行输入操作(read)、输出操作(write)、接受外来连接(accept)和发起外出连接(connect),都是阻塞的!

成果

运行程序,在浏览器地址栏输入:​​http://localhost:8080/​​ ,便可得到下面页面

逐步构建HTTP服务器(一)——构建一个能返回HTTP报文的服务器_响应报文_03

遗憾

  1. 每次客户想连接到服务器必须等上一个客户完成连接建立、发送数据、接受数据和断开连接的过程。