TCP网络编程(一)—— 服务器端模式和客户端模式
这篇文章将会编写基本的服务器网络程序,主要讲解服务器端和客户端代码的原理,至于网络名词很具体的概念,例如什么是TCP协议,不会过多涉及。
首先介绍一下TCP网络编程的两种模式:服务器端和客户端模式:
首先说明一下:黑色线代表状态的转换,红色线表示的是数据的传输,read 和 write 之间的循环表示:例如读取完数据,进入写入的状态,写入完再进入读取的状态,一直循环,实现了服务器和客户端之间的通信。
首先来解释一下服务器端:
int socket(int domain, int type, int protocol)
socket() 表示创建一个套接字。套接字是网络通信的基本数据结构,用于定义通信协议(如 TCP 或 UDP)和地址族(如 IPv4 或 IPv6)。通过套接字,服务器和客户端可以在网络上传输数据,可以把套接字理解为一个编程接口,利用套接字实现程序和网络的连接,像是用户层和传输层(TCP)中间的一个抽象层,有了套接字才可以向网络发送数据。
传入的内容是(协议族,套接字类型,默认协议(通常为0))
返回:成功返回套接字描述符,失败返回-1
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
bind() 表示将套接字绑定到一个特定的地址和端口。绑定的地址和端口标识服务器,使客户端能够找到并连接到该服务。只有套接字还不够,我还要知道是哪个主机(IP)发送的,哪个应用程序(端口)发送的,端口可以理解为电脑通信的入口和出口。
传入的内容是:(套接字描述符,地址结构体的地址,地址结构体大小)
返回:成功返回0,失败返回-1。
int listen(int sockfd, int backlog)
listen() 表示将套接字转换为监听模式,并设置等待连接的队列长度。当多个客户端请求连接时,服务器会将这些请求加入队列,按顺序处理。
传入的内容是(套接字描述符,队列的长度)
返回:成功返回0,失败返回-1。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
accept() 表示等待接受客户端的连接请求,接收到请求,成功连接后,accept() 返回一个新的套接字,用于与该客户端通信,而原始监听套接字则继续处理新的连接请求。
传入的内容是:(套接字描述符,地址结构体的地址,地址结构体大小的地址)
返回:成功返回新的套接字描述符,失败返回-1
ssize_t read(int sockfd, void *buf, size_t count)
read() 表示从套接字描述符中读取数据,用于接收客户端发送的消息。读取的数据存储在提供的缓冲区中。
传入的内容是:(套接字描述符,缓冲区指针(数组),要读取的字节数)
返回:成功返回实际读取的字节数,失败返回-1。
ssize_t write(int sockfd, const void *buf, size_t count)
write() 表示向套接字描述符中写入数据,用于向客户端发送响应数据。
传入的内容是:(套接字描述符,缓冲区指针(数组),要写入的字节数)
返回:成功返回实际写入的字节数,失败返回-1。
int close(int sockfd)
close()表示关闭套接字描述符。
传入的内容是:(套接字描述符)
返回:成功返回0,失败返回-1。
接着解释一下客户端的新出现的函数:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
connet()表示客户端向服务器发起连接请求。客户端告诉操作系统需要连接到哪个服务器的哪个端口。
传入的内容是:(套接字描述符,地址结构体的地址,地址结构体大小)
返回:成功返回0,失败返回-1。
看完这些,你会发现:套接字描述符和文件描述符很像,都可以根据描述进行写入读取和各种其他操作,其实,这就是UNIX系统和类UNIX系统(Linux系统)的抽象资源管理方式,通过整数来标识系统中的资源,使用统一的接口设计,“一切皆文件”。
看到这里,你一定有几个问题:
1.为什么客户端少了bind()和listen()的操作?
2.为什么connect操作指向了accept操作之后?
3.地址结构体的地址addr是个什么东西?
4.为什么有的函数传addr大小,有的传addr大小的地址?
1.对于服务器端来说,服务器需要绑定到固定的端口这样客户端才能知道它,对于客户端来说,操作系统会在必要的时候分配临时的本地端口和地址,不需要再绑定端口。
2.因为服务器端的accept函数是阻塞的,等待客户端发起请求,当connect发送给服务器端请求之后,才会继续进行后面的读写操作。
3.addr的类型如下:有两个成员,分别是地址族,地址和端口信息,但是这不方便我们进行设置,所以一般采用 sockaddr_in 这个结构,最后在进行强制类型转换得到sockaddr,注意这两个结构体类型大小是一样的,只是结构不一样。
struct sockaddr {
sa_family_t sa_family; // 地址族,例如 AF_INET(IPv4)或 AF_INET6(IPv6)
char sa_data[14]; // 地址和端口信息
};
下面是sockaddr_in结构体类型,可以清楚地看到每个成员的含义:
struct sockaddr_in {
sa_family_t sin_family; // 地址族,通常为 AF_INET(IPv4)
uint16_t sin_port; // 端口号(16 位),以网络字节序表示
struct in_addr sin_addr; // IP 地址(32 位)
char sin_zero[8]; // 保留字段,填充用
};
4.可以看到accept函数的addrlen参数是 addr 大小(变量)的地址,但是connect和bind函数的addrlen参数是 addr 大小(变量)本身,这是因为accept不知道调用者提供的 addr 缓冲区的大小可能是IPv4可能是IPv6,所以需要地址地址。
我猜测可能和TCP的三次握手或者accept返回新的套接字或者客户端分配动态端口有关系,而connect和bind函数都是用已知的套接字进行操作,所以不会进行addr大小的改变,所以可以直接传值。
这就是TCP编程的两种模式,从下篇文章开始,我们将学习如何编写服务器端和客户端的代码。
这就是文章的所有内容了,希望对你有所帮助,如有错误欢迎指出。