实现TCP发送HTTP请求

30 阅读3分钟

HTTP协议是最被广泛使用的网络传输协议,所有的www文件都必须遵守这个标准,并且,HTTP基于TCP/IP通信协议来传递数据,本文的主要内容是介绍HTTP请求的的格式,并且使用C语言以TCP的方式来实现请求的发送。

创建套接字

HTTP协议是应用层的协议,而其传输接口层的实现需要通过创建套接字。

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);//http协议默认是80端口
  sin.sin_addr.s_addr = inet_addr(ip);//将char*转化为无符号int

  if (0 != connect(sockfd, (struct sockaddr*)&sin, sizeof(struct sockaddr_in))) {
    return -1;
  }

  fcntl(sockfd, F_SETFL, O_NONBLOCK);//设置为非阻塞
  
  return sockfd;
}

关键技术阐述:上述代码的第3行调用socket函数用以创建套接字描述符,其中第一个参数指定使用IPv4, 第二个参数SOCK_STREAM指定后,默认使用TCP(而不是UDP)协议进行传输,第三个参数一般是具体协议选择,设置为0让系统自行决定即可。

5~12行为相对固定的编程套路,填充好套接字地址sockaddr_in这个结构体的相关内容,与本文相关唯一值得注意的点是,HTTP默认使用80端口。

第14行使用fcntl(file control函数)将连接设置为非阻塞。如果socket是阻塞的,read()的时候整个程序挂起,等待io数据到来,反之则不会。使用到的时候查询下这个函数的参数如何设置即可,不需要特别记忆。

HTTP请求报文的一般格式

image.png

发送http请求的函数实现如下

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,
	  CONNECTION_TYPE
	  );

  send(sockfd, buffer, strlen(buffer), 0);

  //select负责检测网络io里面有没有可读的数据
  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) {
    //用法:select(maxfd+1, &rset, &wset, &eset, NULL);
    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) {
	break;
      }

      result = realloc(result, (strlen(result) + len + 1));
      strncat(result, buffer, len);
    }
  }
  return result;
}

关键技术阐述:第6~15行:像TCP这种协议,数据的传输是一串一串的发送和到来的,更准确的描述是数据流,所以为了应对数据流断断续续到达的情况,需要使用到缓冲区buffer。而http请求内容符合我们上面那张图的格式即可。这里我们为了可读性和可维护性,使用了两个define:

#define HTTP_VERSION "HTTP/1.1"
#define CONNECTION_TYPE "Connection: close\r\n"

19~22行,之前讲过,本文中采用的是非阻塞的实现(read的时候不会挂起,系统继续其他的工作),所以我们可以搭配使用select函数(用于监听socket描述符)实现事件驱动,避免了忙轮询占用大量系统资源。有关细节可以看我的另一篇文章:CSAPP网络编程章节实验:Proxylab

第24~26行设置了一个超时的时间为5s,超过这个时间断开连接。避免无意义的等待。

第31~41行用while(1)循环反复读取服务器响应的内容,直到len等于0读取完毕打破循环退出。