基于 TCP 的服务器端和客户端

基于 TCP 的服务器端和客户端

上一篇中了解了协议族、套接字类型、TCP 的传输特性、地址信息有哪些、地址信息如何表示、如何初始化以及如何绑定到套接字上进行理解。

此篇会对流程中的后几个流程进行理解,以及实现 TCP 服务器端和客户端的迭代代码程序。

等待连接请求状态和受理请求

将套接字绑定号分配的地址之后,需要调用 listen 函数进入等待连接请求的状态。只有调用了 listen 函数,客户端才能进入可发出连接请求的状态,也就是客户端才能调用 connect 函数。 客户端发送连接请求后,客户端就会进入等候队列。这些都是由服务器端的套接字来完成,简单的理解:服务器端套接字相当于 HR,客户端相当于求职者,调用 listen 函数类似于 HR 发布一个工作招聘岗位,此时 HR 就处于等待简历的状态;调用 connect 相当于求职者发送简历,此时我们就进入等候队列,等待 HR 的通过。

等候队列的大小是由 listen 函数的第二个参数来决定,一但发送连接请求的客户端超过此队列大小,服务器端就会自动结束。就如同,一旦 HR 接收的求职者超过他的预期,可能就会关闭招聘岗位。

#include <sys/socket.h>

// sock:希望进入等待连接请求状态的套接字文件描述符,传递的描述符套接字参数称为服务器端套接字
// backlog:连接请求等待队列的长度,若为 5,则队列长度为 5,表示最多使 5 个连接请求进入队列
int listen(int sock, int backlog);

连接请求受理是按序受理,调用 accept 函数后,服务器端会从等候队列中按发送连接请求的先后顺序来受理。受理后需要,意味着服务器端和客户端可以进行数据通信,此时就需要一个新的 fd 与之绑定,通过这个 fd 与对应的客户端进行数据通信。类似于,HR 按序同意求职者的简历,此时就需要一个面试官与求职者进行面试,每一个求职者和面试官是一一对应的(现实中可能不是一个求职者一个面试官,此处忽略),这是和求职者交互的一个通道。

#include <sys/socket.h>

// sock:服务器套接字的文件描述符
// addr:保存发起连接请求的客户端地址信息的变量地址值,调用函数后向传递来的地址变量参数填充客户端地址信息
// addrien:第二个参数 addr 结构体的长度,但是存有长度的变量地址,填充客户端地址长度
int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);

在这里插入图片描述
了解完这些流程,TCP 服务器端和客户端之间的调用流程如下所示

在这里插入图片描述
第一篇实现的服务器端和客户端就是基于 TCP 实现,但是此程序只能处理一个客户端程序,连接请求的等待队列就没有实际意义。那么如何将原先的程序扩展到可以按序处理多个客户端 —— 最简单的方式:将受理连接请求和数据处理等操作放置在一个循环中(由于目前的代码都是单进程单线程的,所以同一时刻只能处理一个客户端)。程序的流程如下

在这里插入图片描述

迭代服务器端的实现

处理多个客户端时要求服务器端的程序发生改变,客户端不需要变化,服务器端的程序如下(Windows 中实现基本相同,此处不再展现)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>

int main(int argc, char *argv[]) {
  if (2 != argc) {
    fprintf(stderr, "Usage: %s <port>\n", argv[0]);
    exit(EXIT_FAILURE);
  }

  // 1. 创建套接字
  int sockfd = socket(AF_INET, SOCK_STREAM, 0);
  if (-1 == sockfd) {
    perror("socket() error");
    exit(EXIT_FAILURE);
  }

  // 2. 绑定地址信息
  struct sockaddr_in serv_addr;
  memset(&serv_addr, 0, sizeof(serv_addr));
  serv_addr.sin_family = AF_INET;
  serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
  serv_addr.sin_port = htons(atoi(argv[1]));
  if (-1 == bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr))) {
    perror("bind() error");
    close(sockfd);
    exit(EXIT_FAILURE);
  }

  // 3. 打开可连接状态,进入监听
  if (-1 == listen(sockfd, 5)) {
    perror("listen() error");
    close(sockfd);
    exit(EXIT_FAILURE);
  }

  struct sockaddr_in clnt_addr;
  memset(&clnt_addr, 0, sizeof(clnt_addr));
  socklen_t clnt_len = sizeof(clnt_addr);
  for (int i = 0; i < 5; ++i) {
    int clntfd = accept(sockfd, (struct sockaddr *)&clnt_addr, &clnt_len);
    if (-1 == clntfd) {
      perror("accept() error");
      close(sockfd);
      exit(EXIT_FAILURE);
    } else {
      printf("Connected clinet: %d\n", clntfd);
    }

    char message[1024] = {0};
    int str_len = 0;
    while (0 != (str_len = read(clntfd, message, 1024)))
      write(clntfd, message, str_len);

    close(clntfd);
  }
  close(sockfd);

  return 0;
}

此程序可以实现按序处理多个客户端,在某一时刻只能处理一个,要想处理下一个,必须将当前处理的客户端断开才行。

TCP 原理

首先要理解,TCP 程序中 readwrite 函数并不是说调用一次就直接发送数据或接收数据的,在这中间是有两个缓冲区的,如下所示

在这里插入图片描述
这些缓冲区有几个特性:

  • I/O 缓冲在每个 TCP 套接字中单独存在
  • I/O 缓冲在创建套接字时自动生成
  • 即使关闭套接字也会继续传递输出缓冲中遗留的数据
  • 关闭套接字将会丢失输入数据中的数据

现在进一步可以知道,write 函数是在数据移到输出缓冲时,才会返回。关于 TCP 的三次握手和四次挥手此处不做详细说明,请自行查阅理解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xjjeffery

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值