typechat聊天项目构建第一步!echo server

ehco server是聊天系统的细胞核

自己和自己聊天,就是echo server

它包含了聊天应用最底层的一切:建立连接 → 接收数据 → 处理数据 → 返回数据 → 关闭连接。把这个流程跑通,后面的所有功能(登录、好友、群聊、文件传输)都只是在这个骨架上加肉。

上网聊天的本质是什么
打字“你好” -> 网路传输 -> 对方收到“你好”
也就是客户端 write(“你好”) → TCP 传输 → 服务端 read() → 服务端 write(“你好”) → 另一个客户端 read()

证明了数据能在两台机器之间传输,接下来就是如何实现这个功能

1. 了解头文件分工

参考,用了哪些头文件,头文件提供了哪些功能,白话解释

头文件 提供了哪些功能 白话
memset 把内存清空
cout/cerr 往屏幕里面打印东西
<sys/socket.h> socket()/bind()/listen()/accept() 创建和绑定套接字,是网络通信核心函数
<netinet/in.h> sockaddr_in结构体,htons() 存IP+端口的数据结构
<arpa/inet.h> inet_ntop() 把二进制的IP地址转换成点分十进制的IP地址
<unistd.h> close() 关闭套接字
<fcntl.h> fcntl() 设置套接字为非阻塞
<sys/epoll.h> epoll_create()/epoll_ctl()/epoll_wait() epoll多路复用
<sys/types.h> socklen_t 数据类型

2. 创建socket的实现方法

1
int socket(int domain, int type, int protocol);
  • domain: 协议族,ipv4使用AF_INET
  • type: 套接字类型,SOCK_STREAM表示TCP
  • protocol: 协议,0表示默认协议

返回server_fd:套接字文件描述符,在linux中一切皆文件,套接字也不例外,后面的操作都通过这个数字来进行


3.sockaddr_in结构体————地址结构体

1
2
3
4
5
6
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
};

舒适化addr_in结构体,对三个字段逐个进行设置

1
2
3
4
struct sockaddr_in addr;
addr.sin_family = AF_INET; //协议族,告诉系统我使用的是什么协议(ipv4)
addr.sin_port = htons(8080); //端口号,监听本机所有ip,端口号8080
addr.sin_addr.s_addr = INADDR_ANY; //ip地址,监听本机所有ip,INADDR_ANY表示本机所有ip

为什么要使用htons()函数?

  • htons()函数将主机字节序转换成网络字节序
  • 主机字节序:小端序,低地址存低位,高地址存高位
  • 网络字节序:大端序,低地址存高位,高地址存低位
  • 为什么要转换?因为网络传输数据时,数据是以大端序传输的,所以需要转换成大端序

4.bind()函数————对socket进行绑定

1
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd: 套接字文件描述符,刚才socket()函数返回的
  • addr: 地址结构体,刚才addr_in结构体设置好的
  • addrlen: 地址结构体长度,sizeof(addr)告诉函数要读多少字节

5.listen()函数————开始监听

1
int listen(int sockfd, int backlog);
  • sockfd: 套接字文件描述符,刚才socket()函数返回的
  • backlog: 等待队列长度,表示最多有多少个客户端可以排队等待连接

6.accept()函数————接受客户端连接

1
2
3
4
5
6
7
sockaddr_in client_addr{};                        // 空的,等 accept 往里填
socklen_t client_len = sizeof(client_addr); // 结构体大小,必须传

int client_fd = accept(server_fd, // 从哪个监听 socket 接受
(sockaddr*)&client_addr, // 输出:客户端信息存这里
&client_len); // 输入输出:结构体大小

  • server_fd: 监听套接字文件描述符,刚才socket()函数返回的
  • client_addr: 客户端地址结构体,accept 会往里填写客户端信息
  • client_len: 客户端地址结构体长度,必须传,accept 会修改这个值

client_fd与server_fd的区别:

  • accept()函数会返回一个新的套接字文件描述符,这个套接字文件描述符专门用来和客户端通信,和server_fd没有关系
  • server_fd是用来监听的,client_fd是用来通信的
  • client_fd是专属的服务员,只服务一个人,收发数据,用完就关闭
  • server_fd是公共的,只负责接客,不负责服务,永远监听

accept()是阻塞的,如果没有客户端连接,accept()会一直等待,直到有客户端连接为止


7. 打印客户端信息

1
2
3
4
char client_ip[INET_ADDRSTRLEN] = {0}; // 存储客户端IP地址
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN); // 将二进制IP地址转换成点分十进制的IP地址
cout << "client ip: " << client_ip << endl; // 打印客户端IP地址

inet_ntop()与inet_pton()的区别:

  • inet_ntop()是将二进制的IP地址转换成点分十进制的IP地址 ,network to presentation
  • inet_pton()是将点分十进制的IP地址转换成二进制的IP地址 ,presentation to number
    ntohs与htons的区别
  • ntohs是将网络字节序转换成主机字节序,network to host
  • htons是将主机字节序转换成网络字节序,host to network

8.收发循环

1
2
3
4
5
6
7
8
while (true) {
char buffer[1024] = {0}; // 在栈上分配1024字节的内存,用于存储接收到的数据
int bytes_read = read(client_fd, buffer, sizeof(buffer) - 1);
// ^^^^^^^^^^^^^^^^^
// 留 1 个字节给 '\0',确保是合法字符串

}

read()的三种返回值:

返回值 含义 处理方式
>0 读到了N个字节 正常处理数据
=0 客户端关闭了连接 break退出循环
<0 读取出错了 打印错误信息,break退出循环

9.关闭套接字

1
2
close(client_fd); // 关闭客户端套接字
close(server_fd); // 关闭服务器套接字

不关的话下一次用会显示端口被占用,先关闭客户端套接字,再关闭服务器套接字

简要概述整个流程

socket() → 创建水管,拿到文件描述符 server_fd = 3

bind() → 把水管接在端口 8080 上

listen() → 竖起耳朵,最多排队 3 个

accept() → 阻塞等待,有人连接时返回 client_fd = 4

read() → 阻塞等待客户端发数据

write() → 把数据原样发回去

close() → 关闭两个文件描述符,释放资源

server_fd = 3 (前台,只管接客)
client_fd = 4 (服务员,和具体客户聊天)