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 | struct sockaddr_in { |
舒适化addr_in结构体,对三个字段逐个进行设置
1 | struct sockaddr_in addr; |
为什么要使用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 | sockaddr_in client_addr{}; // 空的,等 accept 往里填 |
- 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 | char client_ip[INET_ADDRSTRLEN] = {0}; // 存储客户端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 | while (true) { |
read()的三种返回值:
| 返回值 | 含义 | 处理方式 |
|---|---|---|
| >0 | 读到了N个字节 | 正常处理数据 |
| =0 | 客户端关闭了连接 | break退出循环 |
| <0 | 读取出错了 | 打印错误信息,break退出循环 |
9.关闭套接字
1 | close(client_fd); // 关闭客户端套接字 |
不关的话下一次用会显示端口被占用,先关闭客户端套接字,再关闭服务器套接字
简要概述整个流程
socket() → 创建水管,拿到文件描述符 server_fd = 3
↓
bind() → 把水管接在端口 8080 上
↓
listen() → 竖起耳朵,最多排队 3 个
↓
accept() → 阻塞等待,有人连接时返回 client_fd = 4
↓
read() → 阻塞等待客户端发数据
↓
write() → 把数据原样发回去
↓
close() → 关闭两个文件描述符,释放资源
server_fd = 3 (前台,只管接客)
client_fd = 4 (服务员,和具体客户聊天)