我们知道HTTP依赖于面向连接的TCP进行消息传递。所以我们实际上是要构建一个能够接受TCP连接并通过TCP发送HTTP报文给用户浏览器的服务器。
如何构建一个TCP服务器?
《UNIX网络编程》 第四章 基本TCP套接字编程
作为服务器,我们要做的就是:1.接受TCP连接请求,2.接收客户发的数据(即HTTP请求报文),3.并回传数据(即HTTP响应报文),4.随后断开该连接。
一个典型的TCP客户端和服务器交互过程以及用到的函数:
- 接受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);
- 接收客户发的数据(即HTTP请求报文)
char buf[MAXLINE]; // read()用来接收数据,并返回收到数据的字节数 // 若用户没有发送数据,将一直阻塞在这里 int n = read(connfd, buf, MAXLINE);
在这里为了简单,我们不对HTTP请求报文做解析,也就是说对任何的HTTP请求都返回相同数据。 - 并回传数据(即HTTP响应报文)
// write()用来向客户发送数据 // write()也会阻塞在此直至数据发送完成 write(connfd, package.c_str(), package.size());
这里package是c++的string对象。
我们需要先了解一下HTTP响应报文:
我们根据格式写了一个响应报文: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;
- 随后断开该连接
close(connfd); }
需要注意的地方
- 1.接受TCP连接请求,2.接收客户发的数据,3.并回传数据,4.随后断开该连接,四个步骤都在一个while(1)中。
while(1) { accept(); read(); write(); close(); }
这样我们无意中实现了一个服务器可以连接多个客户端,虽然只是每一时刻与一个客户端连接。 - 我们获得的套接字是阻塞的。
若一个套接字是阻塞的,就意味着对它进行输入操作(read)、输出操作(write)、接受外来连接(accept)和发起外出连接(connect),都是阻塞的!
成果
运行程序,在浏览器地址栏输入:http://localhost:8080/ ,便可得到下面页面
遗憾
- 每次客户想连接到服务器必须等上一个客户完成连接建立、发送数据、接受数据和断开连接的过程。