目录
什么是TCP协议?
三次握手和四次挥手
TCP通信流程:
socket():
bind():绑定函数
listen():监听函数
accept():和客户端建立连接
connect():客户端连接服务器函数
read()/recv():读取函数
write() /send():写入函数
close():关闭连接
为什么服务器端有bind()函数而客户端没有?
什么是TCP协议?
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的传输协议,它是OSI(Open System Interconnection,开放式系统互联)模型中的第四层协议,通常使用于网络中的应用层和传输层之间。
TCP协议的特点有:
1. 面向连接:通信前需要先建立连接,传输完成后再释放连接。传输数据时有三次握手和四次挥手过程。
2. 可靠传输:采用确认应答机制和重传控制,确保数据传输的可靠性和完整性。
3. 流量控制:通过滑动窗口技术,控制发送方发送数据的速度,避免接收方处理不过来而出现数据丢失的情况。
4. 拥塞控制:根据网络拥塞情况调整发送数据的速度。当网络拥塞时,TCP协议会通过减小发送窗口或延迟发送数据的方式来降低网络拥塞程度。
5. 面向字节流:TCP协议是以字节流的形式传输数据,而不是以数据包为单位传输。因此,应用层需要自己处理数据包的边界。
TCP协议常用于HTTP、FTP、Telnet等传输层协议中,因为这些协议需要传输大量的数据并要求传输的数据准确无误。
三次握手和四次挥手
TCP三次握手和四次挥手是TCP协议的基本特性,用于在建立和终止TCP连接时进行协商。以下是TCP三次握手和四次挥手的详细步骤:
TCP三次握手:
1. 客户端向服务器发送一个 SYN 报文段,请求建立连接。
2. 服务器收到 SYN 报文段后,回复一个 SYN ACK 报文段,表示收到请求并准备好连接。
3. 客户端收到 SYN ACK 报文段后,回复一个 ACK 报文段,表示确认连接已建立。
TCP四次挥手:
1. 客户端向服务器发送一个 FIN 报文段,请求关闭连接。
2. 服务器收到 FIN 报文段后,回复一个 ACK 报文段,表示已经收到请求。
3. 服务器在关闭连接前,需要先发送一个 FIN 报文段,表示可以关闭连接。
4. 客户端收到 FIN 报文段后,回复一个 ACK 报文段,表示已经收到请求,然后在一段时间后关闭连接。
需要注意的是,TCP协议通过三次握手建立连接后,客户端和服务器之间可以进行多次数据传输。在数据传输完成后,通过四次挥手来关闭连接。
TCP通信流程:
TCP通信流程大致如下:
1. 建立连接:发送方和接收方通过三次握手建立TCP连接。首先,发送方向接收方发送SYN(同步)报文,表示请求建立连接;接收方收到SYN报文后,向发送方回送ACK(确认)和SYN报文,表示接收方已经准备好建立连接;发送方再回送ACK报文,表示连接建立成功。
2. 数据传输:连接建立成功后,发送方可以向接收方发送数据。TCP协议会将数据分成若干个数据段,每个数据段有编号,并且会保证按序传输,确保数据的可靠性。
3. 确认接收:接收方收到数据后,会向发送方发送ACK确认报文,表示成功接收数据。
4. 关闭连接:一旦数据传输完成,发送方和接收方都可以向对方发送FIN(结束)报文,表示要关闭连接。当一方收到FIN报文后,会回送ACK报文确认收到,同时关闭自己的连接。当另一方收到ACK报文后,也关闭自己的连接,这样双方的连接就完全关闭了。(四次挥手)
socket():
socket()函数用于创建一个新的socket(套接字),并返回其文件描述符。socket()函数的语法如下:
int socket(int domain, int type, int protocol);
//domain:指定socket所属的协议族,常见的取值有AF_INET(IPv4协议)、AF_INET6(IPv6协议)等。
//type:指定socket的类型,常见的取值有SOCK_STREAM(流式套接字,用于TCP协议)和SOCK_DGRAM(数据报套接字,用于UDP协议)等。
//protocol:指定协议类型,通常设置为0(默认值),表示使用默认的协议。
socket()函数返回新创建的socket的文件描述符,如果创建失败,则返回-1并设置errno错误码,表明失败的原因。
需要注意的是,创建socket后,需要调用bind()函数将socket与一个本地地址(IP地址和端口号)绑定,并调用connect()函数或listen()函数启动socket的监听或发起连接。在使用完socket后,需要调用close()函数关闭socket,以释放相关资源。
bind():绑定函数
bind()函数用于将一个socket(套接字)与一个本地地址(IP地址和端口号)绑定。在客户端中,通常不需要使用bind()函数,因为内核会自动分配一个可用端口给客户端socket。但在服务器程序中,需要使用bind()函数将socket与一个已知的IP地址和端口号绑定,以便客户端可以通过该地址和端口号来连接服务器。bind()函数的语法如下:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//sockfd:要绑定的socket文件描述符。
//addr:指向数据结构struct sockaddr的指针,该结构体包含IP地址和端口号信息。
//addrlen:sockaddr结构体的长度
bind()函数返回0表示绑定成功,否则返回-1并设置errno错误码,表明绑定失败的原因。通常情况下,如果没有权限或者指定的端口号已经被占用,bind()函数会失败。
需要注意的是,在服务器程序中,bind()函数通常需要在调用listen()函数之前调用,以便指定监听的地址和端口号。而在客户端程序中,不需要调用bind()函数,因为内核会自动为客户端分配一个可用端口号。
listen():监听函数
listen()函数是用于TCP服务器端的系统调用函数,它将一个套接字标记为被动套接字(passive socket),即该套接字用于等待客户端的连接请求。listen()函数的语法如下:
int listen(int sockfd, int backlog);
//sockfd:要监听的套接字的文件描述符。
//backlog:指定连接请求的队列长度,即等待被服务程序接受的连接请求的最大个数。这个参数的值应该大于等于0,通常取值为5或者10。
listen()函数返回0表示成功,返回-1表示失败,并设置errno错误码,表示失败的原因。
调用listen()函数后,服务器端进程就可以通过accept()函数接受客户端套接字的连接请求。需要注意的是,listen()函数并不阻塞,它仅仅是将套接字标记为被动套接字,并将该套接字加入内核管理的监听列表中,客户端连接请求实际上是在accept()函数中被处理的。
accept():和客户端建立连接
accept()函数是一个阻塞函数,它使服务器进程进入了等待状态,直到有客户端发起连接请求。如果有连接请求到来,该函数会返回一个新的套接字描述符,用于和客户端进行通信。可以在accept()函数调用之前设置一个超时时间,以避免服务器长时间等待连接请求而陷入死循环。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
//sockfd:指定欲等待接受连接的套接字描述符。
//addr:指向用于存放远程主机地址信息的结构体指针,可以为 NULL。
//addrlen:指向一个整型变量,表示 addr 所指向的地址结构体的长度。如果 addr 为 NULL,则该参数无用。
返回值:
- 成功:返回新创建连接的套接字描述符;
- 失败:返回 -1,错误类型存于 errno 中。
请注意,在使用accept()函数时,服务器进程应当针对每一个客户端连接创建一个新的进程或线程,以充分利用系统资源,避免阻塞其他客户端的请求。此外,服务器进程还应该对每个客户端进行必要的身份验证和数据传输的安全处理。
connect():客户端连接服务器函数
connect() 函数用于向指定的地址发起连接请求。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//sockfd:指定客户端使用的套接字描述符。
//addr:指向服务器端地址信息的结构体指针。
//addrlen:表示 addr 所指向的地址结构体的长度。
返回值:
- 成功:0;
- 失败:返回 -1,错误类型存于 errno 中。
read()/recv():读取函数
read() 函数是 UNIX 系统提供的标准 I/O 函数,它用于从文件描述符(包括网络套接字)中读取数据。在使用 TCP 套接字时,read() 函数一般会阻塞,直到接收到足够的数据或发生错误。read() 函数的语法如下:
ssize_t read(int fd, void *buf, size_t count);
//fd 表示要读取的文件描述符
//buf 表示读取到的数据存放的缓冲区
//count 表示要读取的数据大小。
recv() 函数它也用于从网络套接字中读取数据,并且相比于 read() 函数更加常用。在使用 TCP 套接字时,recv() 函数一般也会阻塞,直到接收到足够的数据或发生错误。除此之外,recv() 函数还可以设置一些可选的参数,例如在接收数据时指定 MSG_DONTWAIT 标志可以使 recv() 函数变成非阻塞模式。recv() 函数的语法如下:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
//sockfd 表示要读取的套接字文件描述符
//buf 表示读取到的数据存放的缓冲区
//len 表示要读取的数据大小
//flags 是可选参数,可以用来控制 recv() 函数的行为,例如指定 MSG_DONTWAIT 标志可以使 recv() 函数变成非阻塞模式。
write() /send():写入函数
write() 函数是 UNIX 系统提供的标准 I/O 函数,它用于向文件描述符(包括网络套接字)中写入数据。在使用 TCP 套接字时,write() 函数一般会阻塞,直到数据被写入或发生错误。write() 函数的语法如下:
ssize_t write(int fd, const void *buf, size_t count);
//fd 表示要写入的文件描述符
//buf 表示要写入的数据的缓冲区
//count 表示要写入的数据大小。
send() 函数是 Socket API 中的函数,它也用于向网络套接字中写入数据,并且相比于 write() 函数更加常用。在使用 TCP 套接字时,send() 函数一般也会阻塞,直到数据被写入或发生错误。除此之外,send() 函数还可以设置一些可选的参数,例如在写入数据时指定 MSG_DONTWAIT 标志可以使 send() 函数变成非阻塞模式。send() 函数的语法如下:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
//sockfd 表示要写入的套接字文件描述符
//buf 表示要写入的数据的缓冲区
//len 表示要写入的数据大小
//flags 是可选参数,可以用来控制 send() 函数的行为,例如指定 MSG_DONTWAIT 标志可以使 send() 函数变成非阻塞模式。
close():关闭连接
在使用网络套接字时,close() 函数一般会等待发送缓冲区中的数据全部发送完毕或超时之后才会关闭套接字。close() 函数的语法如下:
int close(int fd);
//fd 表示要关闭的文件描述符。
使用 close() 函数关闭套接字时,还应该注意一些问题:
1. 确保在所有的读写操作都已经执行完毕之后再关闭套接字。
2. 在关闭套接字之前,应该通过调用 shutdown() 函数发送一个 FIN 数据包,以便正确地关闭 TCP 连接。关闭套接字之后,不应该再次通过该套接字进行读写操作。
3. 在使用多线程或多进程时,应该注意避免同时关闭同一套接字,以避免出现竞争条件。
为什么服务器端有bind()函数而客户端没有?
在网络编程中,服务器端必须绑定一个端口号来监听客户端的连接请求。bind() 函数用于给服务器端套接字绑定指定的 IP 地址和端口号,这样客户端才能够通过这个 IP 地址和端口号来连接服务器。
而客户端并不需要绑定指定的 IP 地址和端口号,因为客户端只需要通过指定服务器的 IP 地址和端口号来连接服务器即可。因此,客户端没有 bind() 函数。
总结:本文详细讲述了TCP协议的通信流程和具体的函数如何使用