目录
一、前言
二、基础知识
三、HTTP请求器(TCP客户端)
3.1.HTTP工作原理
3.2.客户端请求消息
3.3.实现HTTP请求器实例
四、TCP服务器
4.1.TCP客户端/服务端开发流程
4.2.I/O多路复用机制
4.2.1.select、poll和epoll的工作原理
4.2.2.触发方式
4.2.3.阻塞和非阻塞的区别
4.3.TCP服务器实例
上文网络编程篇一中的DNS请求是基于UDP通信协议实现的,而本文要实现的HTTP请求器是基于TCP协议实现的。UDP (User Datagram Protocol) 是一种无连接的通信协议,它不保证数据的可靠传输,但是传输速度较快。UDP适用于实时性要求较高的应用,如视频、音频传输等。而TCP (Transmission Control Protocol) 是一种面向连接的通信协议,它保证数据的可靠传输,但是传输速度相对较慢。TCP适用于要求可靠性的应用,如HTTP、FTP等。在本文中,由于进行HTTP请求需要确保数据的可靠传输,因此选择使用TCP协议来实现HTTP请求器。这样可以保证在与服务器进行通信时的数据传输的可靠性和稳定性。
1.HTTP
HTTP(HyperText Transfer Protocol)是一种用于传输超文本数据的应用层协议,它是在Web浏览器和Web服务器之间进行通信的基础。HTTP使用TCP/IP协议作为传输协议,通过可靠的连接来传输数据。
通俗来讲,HTTP是一个在计算机世界里专门在两点之间传递文字、图片、音视频等超文本数据的约定和规范。
HTTP优缺点:
优点:
缺点:
2.HTTPs
HTTPs(HTTP Secure)是在HTTP的基础上添加了安全性的协议,它使用了SSL/TLS协议对HTTP的数据进行加密。通过使用证书和公钥加密技术,HTTPs可以提供对数据的加密和身份验证,从而保护用户隐私和数据的安全性。
相对于HTTP,HTTPs在传输过程中增加了数据的保密性和完整性。它可以防止数据被窃听、篡改和伪造,并且可以验证服务器的身份是否可信。常见的使用HTTPs的场景包括网上银行、电子商务、用户登录等需要保护用户隐私和数据安全的应用。
3.UDP与TCP的区别
UDP和TCP是两种不同的传输层协议,用于在计算机网络中进行数据通信。他们的区别如下:
4.TCP的三次握手,四次挥手?
三次握手是指TCP建立连接的过程,四次握手是指TCP终止连接的过程。握手指客户端和服务端的交互。
TCP的三次握手是为了确保双方建立可靠的通信连接。简单描述如下:
TCP的四次挥手是为了确保双方能够正常关闭连接。简单描述如下:
5.OSI模型
OSI模型是控制计算机和网络设备之间信息交换的标准框架,全称为开放系统互联通信参考模型(Open Systems Interconnection Reference Model)。它于1984年由国际标准化组织(ISO)提出,并被广泛接受和应用。
OSI模型将网络通信过程划分为七个不同的层次,每个层次都有特定的功能和任务。这些层次依次是:
HTTP 协议工作于客户端-服务端架构上。浏览器作为 HTTP 客户端通过 URL 向 HTTP 服务端即 WEB 服务器发送所有请求。常见的Web 服务器有:Apache 服务器,IIS 服务器(Internet Information Services)等。Web 服务器根据接收到的请求后,向客户端发送响应信息。
HTTP 默认端口号为 80,但是你也可以改为 8080 或者其他端口。
HTTP 三点注意事项:
http请求报文格式:客户端发送一个 HTTP 请求到服务器的请求消息包括以下格式:请求行(request line)、请求头部(header)、空行和请求数据四个部分组成,下图给出了请求报文的一般格式。
下面实例是一点典型的使用 GET 来传递数据的实例:
客户端请求:
GET /hello.txt HTTP/1.1
User-Agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3
Host: www.example.com
Accept-Language: en, mi
根据 HTTP 标准,HTTP 请求可以使用多种请求方法。
http响应报文格式:HTTP 响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。
HTTP状态码
当浏览者访问一个网页时,浏览者的浏览器会向网页所在服务器发出请求。当浏览器接收并
显示网页前,此网页所在的服务器会返回一个包含 HTTP 状态码的信息头(server header)用以响应浏览器的请求。
下面是常见的 HTTP 状态码:
这段代码实现了一个简单的HTTP客户端,可以向指定的主机发送HTTP请求并接收响应。
#include #include #include #include #include #include #include #include #include #define HTTP_VERSION "HTTP/1.1" #define CONNETION_TYPE "Connection: close\r\n" #define BUFFER_SIZE 4096 // DNS --> // baidu --> struct hosten char *host_to_ip(const char *hostname) { struct hostent *host_entry = gethostbyname(hostname); //dns // 14.215.177.39 --> //inet_ntoa ( unsigned int --> char * // 0x12121212 --> "18.18.18.18" if (host_entry) { return inet_ntoa(*(struct in_addr*)*host_entry->h_addr_list); } return NULL; } int http_create_socket(char *ip) { int sockfd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in sin = {0}; sin.sin_family = AF_INET; sin.sin_port = htons(80); // sin.sin_addr.s_addr = inet_addr(ip); if (0 != connect(sockfd, (struct sockaddr*)&sin, sizeof(struct sockaddr_in))) { return -1; } fcntl(sockfd, F_SETFL, O_NONBLOCK); return sockfd; } // hostname : github.com --> char * http_send_request(const char *hostname, const char *resource) { char *ip = host_to_ip(hostname); // int sockfd = http_create_socket(ip); char buffer[BUFFER_SIZE] = {0}; sprintf(buffer, "GET %s %s\r\n\ Host: %s\r\n\ %s\r\n\ \r\n", resource, HTTP_VERSION, hostname, CONNETION_TYPE ); send(sockfd, buffer, strlen(buffer), 0); //select fd_set fdread; FD_ZERO(&fdread); FD_SET(sockfd, &fdread); struct timeval tv; tv.tv_sec = 5; tv.tv_usec = 0; char *result = malloc(sizeof(int)); memset(result, 0, sizeof(int)); while (1) { int selection = select(sockfd+1, &fdread, NULL, NULL, &tv); if (!selection || !FD_ISSET(sockfd, &fdread)) { break; } else { memset(buffer, 0, BUFFER_SIZE); int len = recv(sockfd, buffer, BUFFER_SIZE, 0); if (len == 0) { // disconnect break; } result = realloc(result, (strlen(result) + len + 1) * sizeof(char)); strncat(result, buffer, len); } } return result; } int main(int argc, char *argv[]) { if (argc < 3) return -1; char *response = http_send_request(argv[1], argv[2]); printf("response : %s\n", response); free(response); }
下面是对代码的简要解释:
host_to_ip
函数:该函数使用gethostbyname
函数将主机名转换为对应的IP地址。
http_create_socket
函数:该函数创建一个套接字,并使用connect
函数连接到指定的IP地址和端口号(80)。
http_send_request
函数:该函数接收主机名和资源路径作为参数,首先调用host_to_ip
函数获取对应的IP地址,然后调用http_create_socket
函数创建套接字并连接到主机。接下来,根据HTTP协议的格式构建HTTP请求报文,使用send
函数将请求发送到服务器。之后,通过使用select
函数来轮询套接字是否有数据可读,如果有可读数据,则使用recv
函数读取数据并将其存储在缓冲区中,并将缓冲区的内容追加到结果字符串中。最后,返回结果字符串。
main
函数:该函数通过命令行参数接收主机名和资源路径,并调用http_send_request
函数发送HTTP请求并将响应打印出来。
疑问?上面代码中使用的select是什么?有什么用?
回答:
select() 是一个用于多路复用 I/O 的函数。它可以同时监视多个文件描述符,一旦其中一个或多个文件描述符准备好进行 I/O 操作(可读、可写、出错等),select() 函数就会返回。这样可以避免在没有数据可读或写入时阻塞程序。
如果不使用select,而是直接使用阻塞式的recv函数,那么在每次接收数据时都需要等待服务器返回数据,如果服务器的响应时间较长,那么程序会一直阻塞在recv函数的调用处,无法进行其他的操作。
使用select函数可以设置一个超时时间,可以在超时时间内检测socket是否有数据可读,如果没有数据则可以进行其他的操作,避免了阻塞。
综上所述,使用select可以提高程序的并发性能和响应速度,提高了代码的可扩展性。
TCP客户端程序开发流程:
TCP服务端开发流程:
在前面的tcp客户端程序中,在使用recv()函数接收数据时使用了I/O多路复用机制——select。这样避免了使用recv()接收不到数据而出现阻塞,select可以设置超时时间,一段时间未收到数据就会结束该段程序。下面将详细介绍几种常用的I/O复用机制,select、poll和epoll。
select:
poll:
epoll:
1.事件驱动,效率高,具有较低的系统调用开销。
2.高并发,支持较大的并发连接数,可以监听上万个文件描述符。
3.具有较好的可移植性,适用于大部分操作系统。
1.只能在Linux系统下使用:Epoll是Linux内核中的一个特性,因此只能在Linux系统下使用。
2.编程接口相对复杂,使用起来相对困难一些。
疑问:epoll相比于select、poll的优势是什么?
回答:
两种触发方式是边沿触发(Edge Triggered,ET)和水平触发(Level Triggered,LT)。
1.ET边沿触发:
2.LT水平触发:
总结:
阻塞和非阻塞是指线程或进程在执行某个操作时的行为方式。
阻塞:当一个线程或进程执行某个操作时,如果操作不能立即完成,那么线程或进程将会被挂起,等待操作完成后再继续执行后续任务。在这个等待的过程中,该线程或进程无法执行其他任务。
非阻塞:当一个线程或进程执行某个操作时,如果操作不能立即完成,线程或进程不会被挂起,而是立即返回,继续执行后续任务。在这个过程中,该线程或进程可以同时处理其他任务。
在高并发编程中,阻塞方式可能导致线程或进程的资源浪费,因为线程或进程被挂起时无法做其他事情。而非阻塞方式则可以提高系统的并发能力,充分利用线程或进程的资源。因此,非阻塞方式在高并发场景中更加常用。
#include #include #include #include #include #include #include #include #include #define BUFFER_LENGTH 1024 #define EPOLL_SIZE 1024 void *client_routine(void *arg) { int clientfd = *(int *)arg; while (1) { char buffer[BUFFER_LENGTH] = {0}; int len = recv(clientfd, buffer, BUFFER_LENGTH, 0); if (len < 0) { close(clientfd); break; } else if (len == 0) { // disconnect close(clientfd); break; } else { printf("Recv: %s, %d byte(s)\n", buffer, len); } } } // ./tcp_server int main(int argc, char *argv[]) { if (argc < 2) { printf("Param Error\n"); return -1; } int port = atoi(argv[1]); int sockfd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in addr; memset(&addr, 0, sizeof(struct sockaddr_in)); addr.sin_family = AF_INET; addr.sin_port = htons(port); addr.sin_addr.s_addr = INADDR_ANY; if (bind(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0) { perror("bind"); return 2; } if (listen(sockfd, 5) < 0) { perror("listen"); return 3; } // #if 0 while (1) { struct sockaddr_in client_addr; memset(&client_addr, 0, sizeof(struct sockaddr_in)); socklen_t client_len = sizeof(client_addr); int clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len); pthread_t thread_id; pthread_create(&thread_id, NULL, client_routine, &clientfd); } #else int epfd = epoll_create(1); struct epoll_event events[EPOLL_SIZE] = {0}; struct epoll_event ev; ev.events = EPOLLIN; ev.data.fd = sockfd; epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev); while (1) { int nready = epoll_wait(epfd, events, EPOLL_SIZE, 5); // -1, 0, 5 if (nready == -1) continue; int i = 0; for (i = 0;i < nready;i ++) { if (events[i].data.fd == sockfd) { // listen struct sockaddr_in client_addr; memset(&client_addr, 0, sizeof(struct sockaddr_in)); socklen_t client_len = sizeof(client_addr); int clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len); ev.events = EPOLLIN | EPOLLET; ev.data.fd = clientfd; epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev); } else { int clientfd = events[i].data.fd; char buffer[BUFFER_LENGTH] = {0}; int len = recv(clientfd, buffer, BUFFER_LENGTH, 0); if (len < 0) { close(clientfd); ev.events = EPOLLIN; ev.data.fd = clientfd; epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev); } else if (len == 0) { // disconnect close(clientfd); ev.events = EPOLLIN; ev.data.fd = clientfd; epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev); } else { printf("Recv: %s, %d byte(s)\n", buffer, len); } } } } #endif return 0; }
代码讲解:
这段代码是一个简单的TCP服务器程序。它首先创建一个套接字sockfd,并将其绑定到指定的端口上。然后通过调用listen函数将该套接字设置为监听状态,等待客户端的连接。
在传统的实现中,使用了多线程来处理每个客户端连接。当有新的连接到达时,会创建一个新的线程来处理该连接。这部分代码被注释掉了,暂时不考虑。
新的实现中,使用了epoll来处理客户端连接。首先创建了一个epoll实例epfd,并定义了一个epoll_event类型的数组events。然后将sockfd添加到epoll实例中,监听读事件(EPOLLIN)。
进入主循环,调用epoll_wait函数等待事件发生,最多等待5秒。当事件发生时,会返回就绪的事件数量nready。接下来就是遍历就绪事件的过程。
如果就绪事件是sockfd(监听事件),说明有新的客户端连接到达。通过accept函数接收新的客户端连接,并将该连接的文件描述符添加到epoll实例中,监听读事件(EPOLLIN | EPOLLET)。
如果就绪事件是客户端连接的文件描述符,说明有数据可读。通过recv函数读取数据,并进行处理。如果读取失败或者读取到了0字节,说明连接已断开,将该连接的文件描述符从epoll实例中删除。
整个程序的主要思路就是通过epoll来监听多个文件描述符的读事件,实现并发处理多个客户端连接。
上一篇:连接VPN后无法联网