0x07-HTTP_client


0x07-HTTP_client

类型/结构体所在头文件作用常用成员备注
in_addr<netinet/in.h>32 位 IPv4 数值s_addr直接 inet_ntoa()
in6_addr<netinet/in.h>128 位 IPv6 数值s6_addr[16]inet_ntop()
sockaddr_in<netinet/in.h>IPv4 完整地址+端口sin_family,
sin_port,
sin_addr
传给
connect/bind
sockaddr_in6<netinet/in.h>IPv6 完整地址+端口sin6_family,
sin6_port,
sin6_addr
同上
sockaddr_un<sys/un.h>Unix 域路径sun_family,
sun_path
本地进程 IPC
addrinfo<netdb.h>getaddrinfo 结果节点ai_addr,
ai_next,
ai_family,
ai_socktype
现代推荐
hostent<netdb.h>gethostbyname 结果h_name,
h_addr_list
已过时
sockaddr<sys/socket.h>通用抽象基类无数据,仅用于强制转换所有地址结构的“父类”
sockaddr_storage<sys/socket.h>足够大的通用容器无成员,直接强转避免缓冲区溢出
  1. “sockaddr 是基类,sockaddr_in 是子类,强制转换就完事。”
  2. “IPv4 用 sockaddr_in,IPv6 用 sockaddr_in6,通用用 addrinfo。”
  3. “getaddrinfo 一条龙:解析、填结构、连 socket,用完 freeaddrinfo。”
struct addrinfo {
    int ai_flags;       // 额外标志位
    int ai_family;      // AF_INET / AF_INET6 / AF_UNSPEC
    int ai_socktype;    // SOCK_STREAM / SOCK_DGRAM / 0
    int ai_protocol;    // IPPROTO_TCP / IPPROTO_UDP / 0
    socklen_t ai_addrlen;
    struct sockaddr *ai_addr;   // 可直接 bind/connect
    char *ai_canonname; // 若 AI_CANONNAME 置位,则含官方名
    struct addrinfo *ai_next;   // 下一条结果
   };

创建socket + 连接步骤

传统连接步骤

/*=== 创建一个 IPv4、面向字节流(TCP)的 socket ===*/
  int sockfd = socket(AF_INET, SOCK_STREAM, 0); /* AF: Address Families */
  if (sockfd < 0) return -1;
  // sockfd:if (sockfd < 0) return -1; socket 文件描述符
  // AF_INET:  IPv4      AF_INET6: IPv6       AF_UNIX / AF_LOCAL: 本地进程间通信
  // SOCK_STREAM: TCP    SOCK_DGRAM: UDP

  struct sockaddr_in sin = {0}; // 包含 网络协议族(IPv4, IPv6), 端口, 地址 
  sin.sin_family = AF_INET;   // 网络协议族 实际是 unsigned short int == unsigned short  (2 bytes)
  sin.sin_port = htons(80);
  sin.sin_addr.s_addr = inet_addr(ip);     // 上面要传参 ip (char*)--> (unsigned int)
  /* 
  两个函数作用相反:
    inet_addr: char*    --> uint32_t
    inet_ntoa: uint32_t --> char*   
  */

  // 连接服务器
  if (connect(sockfd, (struct sockaddr*)&sin, sizeof(struct sockaddr_in))) {
    fprintf(stderr, "connect failed!\n");
    close(sockfd);
    return -1;
  }

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

使用 addrinfo 一步到位

还能遍历结果集中所有 ip, 直至连接成功

struct addrinfo hints = {0}, *res = NULL;
hints.ai_family = AF_INET; // AF_UNSPEC: 可同时支持 IPv4和IPv6
hints.ai_socktype = SOCK_STREAM;


// 直接使用 hostname 获取sockaddr格式的地址结果 , 不需要涉及 点分ip和二进制ip的转化
int ret = getaddrinfo(hostname, service, &hints, &res); // res 是一个包含所有结果的链表
if (ret) {
  fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(ret));
  return -2;
}

int sockfd = -1; // 要作为返回值 return 出去
struct addrinfo* p = res;

for (; res != NULL; res = res->ai_next) {

  /* 创建 socketfd */ //==>  直接使用 addrinfo 成员变量作为参数! 好用!
  sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
  if (sockfd < 0) continue;

  /* 直到连上一个合适的, 才break, 没连上记得 close */
  if (connect(sockfd, p->ai_addr, p->ai_addrlen) == 0) break;
  else close(sockfd);
}

if (!p) {
  perror("connect failed!\n");
  return -3;
}

freeaddrinfo(res);

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

完整流程: 创建连接 ->发送请求包

char* http_send_request(const char* hostname, const char* resource) {

  // char* ip = host_to_ip(hostname);
  // printf("ip = %s\n", ip);
  // if (!ip) return NULL;
  // int sockfd = http_create_socket(ip);
  // if (sockfd < 0) return NULL;
//==> 可以全部换成下面这一行
  int sockfd = http_connect(hostname, "8888");
  if (sockfd < 0) return NULL;

  /*  tcp 连接已创建   */

// 接下来 组织 http 请求报文
  char req[REQ_SIZE] = {0};
  int req_len = snprintf(req, REQ_SIZE,  // \r回车, \n换行 
  "GET %s %s\r\n"
  "HOST: %s\r\n"    
  "%s\r\n"
  "\r\n",
  resource, HTTP_VERSION, hostname, CONNECTION_TYPE);

  if (send(sockfd, req, strlen(req), 0) != req_len) {
    fprintf(stderr, "send failed!\n");
    close(sockfd);
    return NULL;
  }

  // 由于I/O非阻塞, recv()的话很快就过去了, 根本收不到数据
  //=> 使用 select: `监听` `检测` 网络 I/O 是否返回可接收的数据
// fd_set: FD集合, 也就是一堆 I/O, |>使用 select 需先定义 fd_set<|

  fd_set fdread; // fd_set中, fd作为下标, 元素一旦置1, 就说明该下标对应 fd 有数据可读

  
  // 设置 select 超时时间: 5s 后自动返回 0
  struct timeval tv;
  tv.tv_sec = 5;
  tv.tv_usec = 0;
  
  size_t total = 0;
  char* buffer = calloc(1, BUFFER_SIZE); // 用于分次搬运的小货车
  char* result = calloc(1, sizeof(int)); // 由于最终会作为send_request函数的返回值, 留到 main 函数内, 调用者必须自主 free 
  while (1) {
    // 先置空, 每次循环都必须重置 fdread
    FD_ZERO(&fdread);
    FD_SET(sockfd, &fdread);
    
    
    
    // 非阻塞情况下, recv 立即返回, 需要while(1)循环recv才能接收数据, 此时会导致忙等待
    // select 会自动监控多个fd (这里就1个), 没有fd的时候会挂起, 不占CPU (超过timeout时间, 自动返回), 一旦有fd就绪, 立刻返回就绪的fd个数
    // 5个参数: select(maxfd + 1, &rset`关注哪些IO可读`, &wset`关注哪些IO可写`, &eset`关注哪些IO出错`, NULL `timeout: select多久算超时`)
    // 返回值: 就绪 fd 的个数
    int selection = select(sockfd + 1, &fdread, NULL, NULL, &tv);
    // 因为我们就一个FD, 如果 被设置的FD不是sockfd, 那肯定有错
    if (!selection || !FD_ISSET(sockfd, &fdread)) { // 超时<---大概率 (或者 fd 不是sockfd)
      fprintf(stderr, "select timeout\n");
      break;
    } else {

      int len = recv(sockfd, buffer, BUFFER_SIZE, 0);
      if (len <= 0) {    // == 0: 代表 disconnect      < 0 (-1): 代表出错
        break;
      }
// len > 0: 有数据
      // 动态扩容, 每接收 len 长度就多realloc len的大小
      result = realloc(result, (strlen(result) + len + 1) * sizeof(char)); 
      // +1 是为了接收最后一个终止符
      
      memcpy(result + total, buffer, len);
      total += len;
      // strncat(result, buffer, len);    // 每次都要遍历一遍result, 性能很差
    }
  }
  free(buffer);
  close(sockfd);
  return result;
}

int main(int argc, char* argv[]) {
  if (argc != 3) {
    fprintf(stderr, "Usage: %s <host> <path>\n", argv[0]);
    return -1;
  }

  char* response = http_send_request(argv[1], argv[2]);
  printf("Response:\n%s\n", response);

  free(response);

  return 0;
}