我们用户是处在应用层的,根据不同的场景和业务需求,传输层就要为我们应用层提供不同的传输协议,常见的就是TCP协议和UDP协议,二者各自有不同的特点,网络中的数据的传输其实就是两个进程间的通信,两个进程在通信时,传输层使用TCP协议将一方进程的应用层的数据传输给另一方进程的应用层,我们这一节就是基于TCP协议讲解网络数据的传输。
目录
一、主机字节序列和网络字节序列
1.1 概念
1.2 接口函数
二、套接字地址结构
2.0 套接字
2.1 通用 socket 地址结构
2.2 专用 socket 地址结构
2.3 IP 地址转换函数
三、网络编程接口
3.1 创建套接字(买个手机)
3.2 套接字地址绑定(为手机办卡,电话号码相当于地址)
3.3 从监听队列中接收一个连接(开机)
3.4 接受客户端连接请求并创建新的套接字 (接听电话)
3.5 客户端主动与服务器建立连接
3.6 从已连接的套接字中接收数据(TCP数据读)
3.6 发送数据到已连接的套接字(TCP数据写)
3.7 从已连接或未连接的套接字接收数据(UDP数据读)
3.8 通过套接字发送数据到指定目标地址((UDP数据写)
3.9 关闭套接字
四、TCP 编程流程
五、三次握手和四次挥手(重点面试题)
5.1 三次握手
5.2 可以将三次握手改成两次握手吗?
5.3 四次挥手
5.4 可以将四次挥手改成三次挥手吗?
六、tcp协议服务器-客户端编程流程实验(掌握)
6.1 服务器端
6.2 客户端
七、实验改进
7.1 服务器端
7.2 客户端
一、主机字节序列和网络字节序列
1.1 概念
主机字节序列分为大端字节序和小端字节序,不同的主机采用的字节序列可能不同。在两台使用不同字节序的主机之间传递数据时,可能会出现冲突。所以,在将数据发送到网络时规定数据使用大端字节序,所以也把大端字节序成为网络字节序列。对方接收到数据后,可以根据自己的字节序进行转换。这是为了确保不同系统之间的数据传输一致性。无论主机字节序列是什么,数据在网络上传输时都需要转换为网络字节序。
- 大端字节序是指:一个整数的高位字节存储在内存的低地址处,低位字节存储在内存的高地址 处。
- 小端字节序是指:整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的 低地址处。
1.2 接口函数
为了在主机字节序列和网络字节序列之间进行转换,编程语言通常提供了一些标准函数。Linux 系统提供如下 4 个函数来完成主机字节序和网络字节序之间的转换:
#include <netinet/in.h>
uint32_t htonl(uint32_t hostlong); // 长整型的主机字节序转网络字节序
uint32_t ntohl(uint32_t netlong); // 长整型的网络字节序转主机字节序
uint16_t htons(uint16_t hostshort); // 短整形的主机字节序转网络字节序
uint16_t ntohs(uint16_t netshort); // 短整型的网络字节序转主机字节序
二、套接字地址结构
2.0 套接字
套接字(Socket)是网络编程中用于描述IP地址和端口的通信端点。它是网络通信中的一个抽象概念,通常用于描述两个程序之间的双向通信链路。套接字是网络编程的基石,允许应用程序通过网络发送和接收数据。
套接字的类型
套接字主要有两种类型:
流式套接字(Stream Socket):
- 使用TCP(传输控制协议)。
- 提供面向连接的、可靠的、基于字节流的通信。
- 典型应用场景包括HTTP、FTP、SMTP等协议。
数据报套接字(Datagram Socket):
- 使用UDP(用户数据报协议)。
- 提供无连接的、不可靠的、基于数据报的通信。
- 适用于需要快速传输且允许丢包的场景,如视频流、在线游戏等。
2.1 通用 socket 地址结构
在网络编程中,通用的 socket 地址结构(Socket Address Structure)用于存储网络地址信息。不同的协议族(如 IPv4、IPv6 等)有不同的地址结构,但都遵循一个通用的框架。socket 网络编程接口中表示 socket 地址的是结构体 sockaddr,这是所有地址结构的通用基础,定义在 <sys/socket.h>
头文件中。它是一个通用的地址结构,包含了地址族信息。
struct sockaddr {
sa_family_t sa_family; // 地址族(Address Family)
char sa_data[14]; // 协议地址(Protocol Address)
};
地址族类型通常与协议族类型对应。常见的协议族和对应的地址族如下图所示:
2.2 专用 socket 地址结构
TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用 socket 地址结构体,它们分别用于 IPV4 和 IPV6:
sockaddr_in
结构体(用于 IPv4)
sockaddr_in
结构体用于存储 IPv4 地址信息。它定义在 <netinet/in.h>
头文件中。结构体定义如下:
struct sockaddr_in {
sa_family_t sin_family; // 地址族(必须为 AF_INET)
in_port_t sin_port; // 端口号(使用 `htons` 将主机字节序转换为网络字节序)
struct in_addr sin_addr; // IP 地址(使用 `inet_pton` 等函数进行赋值)
char sin_zero[8]; // 填充字段,使得结构体大小与 `sockaddr` 一致
};
sin_family
:地址族,必须设置为AF_INET
。sin_port
:端口号,必须使用htons
函数将主机字节序转换为网络字节序。sin_addr
:IP 地址,通常使用inet_pton
或inet_aton
函数进行设置。sin_zero
:填充字段,使得结构体大小与sockaddr
结构体一致,通常设置为 0。
sockaddr_in6
结构体(用于 IPv6)
sockaddr_in6
结构体用于存储 IPv6 地址信息。它也定义在 <netinet/in.h>
头文件中。结构体定义如下:
struct sockaddr_in6 {
sa_family_t sin6_family; // 地址族(必须为 AF_INET6)
in_port_t sin6_port; // 端口号(使用 `htons` 将主机字节序转换为网络字节序)
uint32_t sin6_flowinfo; // 流信息(通常设置为 0)
struct in6_addr sin6_addr; // IPv6 地址
uint32_t sin6_scope_id; // 范围 ID(用于本地链路地址,通常设置为 0)
};
sin6_family
:地址族,必须设置为AF_INET6
。sin6_port
:端口号,必须使用htons
函数将主机字节序转换为网络字节序。sin6_flowinfo
:流信息,通常设置为 0。sin6_addr
:IPv6 地址,使用inet_pton
函数进行设置。sin6_scope_id
:范围 ID,主要用于本地链路地址,通常设置为 0。
2.3 IP 地址转换函数
通常,人们习惯用点分(用点分隔)十进制字符串表示 IPV4 地址,但编程中我们需要先把它们转化为整数(4个字节32位)方能使用,下面函数可用于点分十进制字符串表示的 IPV4 地址和网络字节序整数表示的 IPV4 地址之间的转换(字符串转整型函数接口):
需要引入的头文件 #include <arpa/inet.h>
in_addr_t inet_addr(const char *cp); //字符串表示的 IPV4 地址转化为无符号整型
char* inet_ntoa(struct in_addr in); // IPV4 地址的网络字节序(无符号整型)转化为字符串表示
三、网络编程接口
3.1 创建套接字(买个手机)
int socket(int domain, int type, int protocol);
参数解释
domain: 指定套接字所使用的协议族,也称为地址族。常见的值包括:
AF_INET
:IPv4 Internet 协议AF_INET6
:IPv6 Internet 协议AF_UNIX
或AF_LOCAL
:本地通信(UNIX 域套接字)AF_PACKET
:低级别的套接字接口,用于直接访问网络层type: 指定套接字的服务类型。常见的类型包括:
SOCK_STREAM
:提供面向连接的可靠字节流服务(如 TCP)SOCK_DGRAM
:提供数据报服务(如 UDP)SOCK_RAW
:提供原始网络协议访问SOCK_SEQPACKET
:提供序列包服务,类似于SOCK_STREAM
,但每个消息边界保留protocol: 指定使用的协议。通常设置为 0,以选择默认协议。可以明确指定特定协议:
IPPROTO_TCP
:如果 type 是SOCK_STREAM
IPPROTO_UDP
:如果 type 是SOCK_DGRAM
IPPROTO_ICMP
:如果 type 是SOCK_RAW
(用于原始套接字)返回值
成功时返回一个非负整数,即套接字文件描述符。 失败时返回 -1,并设置
errno
以指示错误。
3.2 套接字地址绑定(为手机办卡,电话号码相当于地址)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
它是一个用于将套接字绑定到特定的本地地址和端口的系统调用。在网络编程中,
bind
函数通常用于服务器端套接字,以指定它们将在哪个地址和端口上监听传入连接。参数解释
sockfd:
- 这是由
socket
函数创建的套接字文件描述符。addr:
- 这是一个指向
struct sockaddr
的指针(通用套接字地址结构体指针),包含要绑定到的地址信息。实际的地址结构根据使用的协议族不同而不同:
- 对于 IPv4,使用
struct sockaddr_in
。- 对于 IPv6,使用
struct sockaddr_in6
。- 对于本地通信(UNIX 域套接字),使用
struct sockaddr_un
。addrlen:
- 这是地址结构的长度(以字节为单位)。利用sizeof求得即可,对于 IPv4 地址,通常是
sizeof(struct sockaddr_in)
;对于 IPv6 地址,通常是sizeof(struct sockaddr_in6)
。返回值
- 成功时返回 0。
- 失败时返回 -1,并设置
errno
以指示错误。
3.3 从监听队列中接收一个连接(开机)
int listen(int sockfd, int backlog);
它是一个用于在指定的套接字上监听连接请求的系统调用。它通常用于服务器端的套接字,以便将套接字转换为被动模式,准备接受来自客户端的连接请求。
参数解释
sockfd:
- 这是由
socket
函数创建并绑定了地址(通过bind
函数)的套接字文件描述符。backlog:
- 在
accept
函数被调用之前可以排队的连接请求数量。在Linux系统上指的是已经完成三次握手的客户端的数量,在unix系统上指的是未完成加已完成的客户端数量。- 如果连接请求的数量超过了此限制,新来的连接请求将被拒绝。
返回值
- 成功时返回 0。
- 失败时返回 -1,并设置
errno
以指示错误。
监听队列可以理解为:客户端向服务器端发送连接请求时,首先,先将它放到监听队列中,让它等着,然后服务器一个一个的从监听队列进行连接,相当于银行大厅的等待区,监听队列的大小就是为等待客户提供的凳子的数量。
3.4 接受客户端连接请求并创建新的套接字 (接听电话)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
它是一个用于接受传入连接请求的系统调用。它通常用于服务器端套接字,用于接受客户端连接请求并创建新的套接字用于与客户端通信。
参数解释
sockfd:
- 这是由
socket
函数创建并绑定了地址(通过bind
函数)的套接字文件描述符。addr:
- 这是一个指向
struct sockaddr
类型的指针,用于存储连接的远程地址信息(客户端的套接字地址信息)。可以将其设置为NULL
,如果不关心连接的远程地址信息。addrlen:
- 这是一个指向
socklen_t
类型的指针,指示传入的地址结构的长度。在调用accept
函数之前,应该将其设置为struct sockaddr
结构的大小。返回值
- 如果成功,返回一个新的套接字文件描述符,也就是连接套接字,用于与客户端通信。
- 如果失败,返回 -1,并设置
errno
以指示错误。
3.5 客户端主动与服务器建立连接
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
它是一个用于连接到远程服务器的系统调用。它通常用于客户端套接字,用于与服务器建立连接。
参数解释
sockfd:
- 这是由
socket
函数创建的套接字文件描述符。serv_addr:
- 这是一个指向
struct sockaddr
类型的指针,包含远程服务器的地址信息。addrlen:
- 这是传入地址结构的长度(以字节为单位)。
返回值
- 如果成功建立连接,则返回 0。
- 如果失败,返回 -1,并设置
errno
以指示错误。
3.6 从已连接的套接字中接收数据(TCP数据读)
ssize_t recv(int sockfd, void *buff, size_t len, int flags);
它是一个用于从套接字接收数据的系统调用。它通常用于在网络编程中从已连接的套接字中接收数据。recv的返回值如果等于0,说明对方关闭了!!!这是循环收发判断的唯一条件!
参数解释
sockfd:
- 这是由
socket
函数创建的套接字文件描述符。buff:
- 这是一个指向接收数据缓冲区的指针,用于存储接收到的数据。
len:
- 这是接收数据缓冲区的长度,即
buff
所指向的缓冲区的大小。flags:
- 这是一组控制接收行为的标志,可以为 0 或以下之一的按位或:
MSG_WAITALL
:阻塞直到接收到指定长度的数据。MSG_DONTWAIT
:非阻塞接收数据。返回值
- 如果成功接收到数据,则返回接收到的字节数。
- 如果连接已关闭,则返回 0。
- 如果发生错误,则返回 -1,并设置
errno
来指示错误。
3.6 发送数据到已连接的套接字(TCP数据写)
ssize_t send(int sockfd, const void *buff, size_t len, int flags);
它是一个用于将数据通过套接字发送到远程端的系统调用。通常在网络编程中,它被用于发送数据到已连接的套接字上。
参数解释
sockfd:
- 这是由
socket
函数创建的套接字文件描述符。buff:
- 这是一个指向要发送数据的缓冲区的指针。
len:
- 这是要发送的数据的字节数。
flags:
- 这是一组控制发送行为的标志,可以为 0 或以下之一的按位或:
MSG_CONFIRM
:要求数据发送得到确认。MSG_DONTROUTE
:数据不路由,仅限于本地接收。MSG_EOR
:数据结束标志。MSG_MORE
:还有更多数据等待发送。MSG_NOSIGNAL
:忽略 SIGPIPE 信号,如果连接已关闭,则不会引发信号。返回值
- 如果成功发送数据,则返回实际发送的字节数。
- 如果发送过程中出现错误,则返回 -1,并设置
errno
来指示错误。
3.7 从已连接或未连接的套接字接收数据(UDP数据读)
ssize_t recvfrom(int sockfd, void *buff, size_t len, int flags, struct sockaddr* src_addr, socklen_t *addrlen);
它是一个用于从已连接或未连接的套接字接收数据的系统调用。与
recv
不同的是,recvfrom
可以从任意地址接收数据,而不仅仅是连接到套接字的对等方。参数解释
sockfd:
- 这是由
socket
函数创建的套接字文件描述符。buff:
- 这是一个指向接收数据缓冲区的指针,用于存储接收到的数据。
len:
- 这是接收数据缓冲区的长度,即
buff
所指向的缓冲区的大小。flags:
- 这是一组控制接收行为的标志,可以为 0 或以下之一的按位或:
MSG_WAITALL
:阻塞直到接收到指定长度的数据。MSG_DONTWAIT
:非阻塞接收数据。MSG_TRUNC
:截断超出缓冲区大小的数据。src_addr:
- 这是一个指向存储发送端地址信息的
struct sockaddr
结构体的指针。addrlen:
- 这是传入地址结构的长度(以字节为单位)。在调用
recvfrom
函数之前,应该将其设置为struct sockaddr
结构的大小。返回值
- 如果成功接收到数据,则返回接收到的字节数。
- 如果连接已关闭,则返回 0。
- 如果发生错误,则返回 -1,并设置
errno
来指示错误。
3.8 通过套接字发送数据到指定目标地址((UDP数据写)
ssize_t sendto(int sockfd, void *buff, size_t len, int flags, struct sockaddr* dest_addr, socklen_t addrlen);
它是一个用于通过套接字发送数据到指定目标地址的系统调用。与
send
不同的是,sendto
允许指定目标地址,因此适用于无连接的 UDP 套接字以及有连接的套接字。参数解释
sockfd:
- 这是由
socket
函数创建的套接字文件描述符。buff:
- 这是一个指向要发送数据的缓冲区的指针。
len:
- 这是要发送的数据的字节数。
flags:
- 这是一组控制发送行为的标志,可以为 0 或以下之一的按位或:
MSG_CONFIRM
:要求数据发送得到确认。MSG_DONTROUTE
:数据不路由,仅限于本地发送。MSG_EOR
:数据结束标志。MSG_MORE
:还有更多数据等待发送。MSG_NOSIGNAL
:忽略 SIGPIPE 信号,如果连接已关闭,则不会引发信号。dest_addr:
- 这是一个指向包含目标地址信息的
struct sockaddr
结构体的指针。addrlen:
- 这是传入目标地址结构的长度(以字节为单位)。在调用
sendto
函数之前,应该将其设置为struct sockaddr
结构的大小。返回值
- 如果成功发送数据,则返回实际发送的字节数。
- 如果发生错误,则返回 -1,并设置
errno
来指示错误。
3.9 关闭套接字
int close(int sockfd);
它是一个用于关闭套接字的系统调用。关闭套接字后,不再可以使用该套接字进行数据传输或接收。
参数解释
- sockfd:
- 这是要关闭的套接字的文件描述符。
返回值
- 如果成功关闭套接字,则返回 0。
- 如果发生错误,则返回 -1,并设置
errno
来指示错误。
四、TCP 编程流程
TCP 提供的是面向连接的、可靠的、字节流服务。TCP 的服务器端和客户端编程流程如 下:
1、socket()方法是用来创建一个套接字,有了套接字就可以通过网络进行数据的收发。
这也是为什么进行网络通信的程序首先要创建一个套接字。创建套接字时要指定使用的服务类型,使用 TCP 协议选择流式服务(SOCK_STREAM)。
2、bind()方法是用来指定套接字使用的 IP 地址和端口。
IP 地址就是自己主机的地址,如果主机没有接入网络,测试程序时可以使用回环地址“127.0.0.1”。端口是一个 16 位的整型值, 一般 0-1024 为知名端口,如 HTTP 使用的 80 号端口。这类端口一般用户不能随便使用。其 次,1024-4096 为保留端口,用户一般也不使用。4096 以上为临时端口,用户可以使用。在 Linux 上,1024 以内的端口号,只有 root 用户可以使用。
3、listen()方法是用来创建监听队列。
监听队列有两种,一个是存放未完成三次握手的连接, 一种是存放已完成三次握手的连接。listen()第二个参数就是指定已完成三次握手队列的长度。
在网络编程中,服务器端通过监听指定的网络地址和端口来等待客户端的连接请求。
监听队列就像是一个等待区,它存放着已经发送连接请求但还没有得到服务器响应的客户端连接请求。当一个客户端请求连接时,服务器将其放入监听队列中,然后按照一定的顺序逐个处理这些请求。通俗地说,你可以把监听队列想象成是一个餐厅的等候区。当你到达餐厅时,可能会看到一个等候区,里面坐满了等待就座的人。服务员会按照先来后到的顺序逐个安排客人入座,就像服务器按照监听队列中连接请求的顺序逐个处理客户端的连接请求一样。
4、accept()处理存放在 listen 创建的已完成三次握手的队列中的连接。
每处理一个连接,则 accept()返回该连接对应的套接字描述符。如果该队列为空,则 accept 阻塞。
5、connect()方法一般由客户端程序执行,需要指定连接的服务器端的 IP 地址和端口。
该方法执行后,会进行三次握手, 建立连接。
6、send()方法用来向 TCP 连接的对端发送数据。
send()执行成功,只能说明将数据成功写入到发送端的发送缓冲区中,并不能说明数据已经发送到了对端。send()的返回值为实际写入到发送缓冲区中的数据长度。
7、recv()方法用来接收 TCP 连接的对端发送来的数据。
recv()从本端的接收缓冲区中读取数 据,如果接收缓冲区中没有数据,则 recv()方法会阻塞。返回值是实际读到的字节数,如果 recv()返回值为 0, 说明对方已经关闭了 TCP 连接。
8、close()方法用来关闭 TCP 连接。
此时,会进行四次挥手。
五、三次握手和四次挥手(重点面试题)
5.1 三次握手
客户端在进行connect()开始建立连接 之后就会进行三次握手!
三次握手是TCP/IP协议中用于建立可靠连接的过程。在进行通信之前,客户端和服务器之间需要通过三次握手来确认彼此的通信能力和参数设置。这个过程包括以下步骤:
客户端发送同步(SYN)报文:客户端首先向服务器发送一个带有SYN标志的TCP报文段,表示客户端想要建立连接,并且指定初始序列号(sequence number)。
服务器确认同步(SYN-ACK)报文:服务器收到客户端的SYN报文后,会向客户端发送一个带有SYN和ACK标志的TCP报文段作为确认。该报文段中也包含服务器选择的初始序列号。
客户端确认(ACK)报文:最后,客户端收到服务器的SYN-ACK报文后,会向服务器发送一个带有ACK标志的TCP报文段作为确认。这个报文段不携带SYN标志。
完成了这三次握手之后,客户端和服务器之间的连接就建立起来了,双方可以开始进行数据传输。这个过程确保了双方都能够收到彼此的确认,从而建立了可靠的通信连接。
下面为面试内容!!
在完成握手时,有两个队列,一个是未完三次握手队列,一个是已完成三次握手队列,客户端请求连接,首先会放到未完三次握手队列,然后等他完成三次握手队列,也就是建立好连接以后,就会将它放到已完成三次握手队列,(注意:listen(socked,5) 在linux里这里的5是代表已完成三次握手队列的大小,在unix里代表未完成和已完成队列之和。这里的5,不是说只能完成5次握手,而是完成握手队列里能放5个链接,第六个就放在未完成队列里,等到完成握手队列里有空位了,在挪下来 ),然后在进行accept()的时候,它会去已完成三次握手队列的里面去看,如果有已经完成三次握手队列的客户端请求,那么他就会与该客户端建立连接,产生一个连接套接字,否则,他会一直阻塞住!(accept只处理已完成握手队列中的链接)
三次握手发生在客户端执行 connect()的时候,该方法返回成功,则说明三次握手已经建 立。三次握手示例图如下:
现在解释这个图:
-
客户端首先向服务器发送一个带有 SYN(同步)标志的报文,表示客户端想要建立连接,并指定初始序列号。这是第一次握手。
-
服务器收到客户端的 SYN 报文后,会发送一个带有 SYN 和 ACK(确认)标志的报文给客户端,表示服务器收到了客户端的请求,并同意建立连接,同时服务器也指定了自己的初始序列号。这是第二次握手。
-
客户端收到服务器的 SYN-ACK 报文后,发送一个带有 ACK 标志的报文给服务器,表示客户端确认收到了服务器的确认,并同意建立连接。这是第三次握手。
5.2 可以将三次握手改成两次握手吗?
不可以!根据TCP协议的设计,三次握手是必需的,并且是建立可靠连接的基础。在标准的TCP实现中,无法将三次握手简化为两次握手。这是因为第三次握手中客户端必须发送ACK包来确认连接建立,以确保双方都能够收到对方的确认信息。
5.3 四次挥手
执行close()之后就会进行四次挥手操作,服务器端和客服端那一端先close()都可以!
三次挥手是TCP/IP协议中用于关闭连接的过程。与建立连接时的三次握手相似,关闭连接时需要进行四次挥手以确保双方都能够完成数据传输并关闭连接,这个过程包括以下步骤:
客户端发送关闭请求(FIN):当客户端决定关闭连接时,它会发送一个带有FIN标志的TCP报文段给服务器,表示它不再发送数据了,但仍然可以接收数据。
服务器确认关闭请求(ACK):服务器收到客户端的关闭请求后,会发送一个带有ACK标志的TCP报文段作为确认,表示它已经收到了客户端的关闭请求。
服务器发送关闭请求(FIN):当服务器确定不再发送数据时,它也会向客户端发送一个带有FIN标志的TCP报文段,表示它也准备关闭连接。
客户端确认关闭请求(ACK):客户端收到服务器的关闭请求后,会发送一个带有ACK标志的TCP报文段作为确认。此时,双方的连接就被完全关闭了。
通过这个四次挥手的过程,客户端和服务器都有机会告知对方它们不再发送数据,并且确认对方的关闭请求,从而安全地关闭连接,避免数据丢失或不完整的传输。
四次挥手发生在客户端或服务端执行 close()关闭连接的时候,示例图如下:
这里是四次挥手的解释:
-
客户端首先发送一个带有 FIN(关闭请求)标志的报文给服务器,表示客户端不再发送数据,但仍然能接收数据。这是第一次挥手。
-
服务器收到客户端的 FIN 报文后,发送一个带有 ACK(确认)标志的报文给客户端,表示服务器已经收到了客户端的关闭请求,但服务器还可以向客户端发送数据。这是第二次挥手。
-
服务器在确定不再发送数据后,发送一个带有 FIN(关闭请求)标志的报文给客户端,表示服务器也准备关闭连接。这是第三次挥手。
-
客户端收到服务器的 FIN 报文后,发送一个带有 ACK(确认)标志的报文给服务器,表示客户端确认收到了服务器的关闭请求。这是第四次挥手。
5.4 可以将四次挥手改成三次挥手吗?
可以,四次挥手可以演化成三次挥手 当一端close 发送报文过来,此时我也要close了,回复报文,和通知对方关闭的报文一起发送。
- 第一次挥手(FIN): 客户端发送一个FIN报文,表示它要关闭到服务器的数据传送。
- 第二次挥手(FIN): 服务器收到FIN后,直接发送一个FIN报文,表示它也要关闭到客户端的数据传送。
- 第三次挥手(ACK): 客户端收到FIN后,发送一个ACK报文,确认收到关闭请求,连接关闭。
六、tcp协议服务器-客户端编程流程实验(掌握)
6.1 服务器端
简单的TCP服务器,它监听6000端口,接收来自客户端的消息,回复“ok”并关闭连接。服务器无限运行,一次处理一个客户端。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0); //创建套接字 ----->监听套接字(相当于饭店的接待员)
if( sockfd == -1 )
{
exit(1);
}
struct sockaddr_in saddr,caddr; //定义服务器套接字地址,客户端套接字地址
memset(&saddr,0,sizeof(saddr)); //清零套接字地址结构体的第四个成员
/*为服务器套接字地址结构体初始化*/
saddr.sin_family = AF_INET; //地址族 IPV4
saddr.sin_port = htons(6000); //端口号 6000
saddr.sin_addr.s_addr = inet_addr("43.138.164.79"); //服务器IP地址;回环地址(用的是测试的本机)
int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr)); //套接字地址绑定
if ( res == -1 )
{
printf("bind err\n"); //这里容易失败,所以要打印观察
exit(1);
}
if (listen(sockfd,5) == -1 ) //从监听队列中接收一个连接
{
exit(1);
}
//服务器无限运行,一次处理一个客户端。
while( 1 ) //循环接收连接
{
int len = sizeof(caddr);
int c = accept(sockfd,(struct sockaddr*)&caddr,&len); //返回值为连接套接字(得到新的套接字描述符),没有人连接时,可能会阻塞!(没有客人来)
if (c < 0 ) //c是套接字文件描述符,相当于服务员,连接失败
{
continue;
}
printf("accept c =%d\n",c );
char buff[128] = {0};
int n = recv(c,buff,127,0); //接收客户端发送过来的数据,如果客户端未发送数据,此时便会阻塞! 读取最多127字节,以留出一个字节用于\0终止符
printf("recv=%s\n",buff);
send(c,"ok",2,0); //服务器给客户端发送数据
close(c); //关闭本次与客户端连接的套接字描述符
}
}
6.2 客户端
实现了一个简单的TCP客户端,它连接到IP地址为
43.138.164.79
、端口为6000
的服务器,读取用户输入,将输入发送到服务器,接收服务器的响应并打印,然后关闭连接。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0); //创建套接字---->监听套接字
if (sockfd == -1 )
{
exit(1);
}
/**注意:客户端不需要绑定套接字地址(调用bind()函数)端口号会随机分配,IP地址就直接用*/
/*客户端需要连接服务器端,因此需要指定连接的服务器的套接字地址,下面的都是服务器的套接字地址信息*/
struct sockaddr_in saddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET; //地址族
saddr.sin_port = htons(6000); //服务器的端口号
saddr.sin_addr.s_addr = inet_addr("43.138.164.79"); //服务器IP地址
int res = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr)); //连接服务器,失败概率高,打印显示
if (res == -1 )
{
printf("connect err\n");
exit(1);
}
char buff[128] = {0};
printf("input:\n");
fgets(buff,128,stdin);
send(sockfd,buff,strlen(buff)-1,0); //客户端发送数据给服务器,客户端不分监听套接字和连接套接字
memset(buff,0,128);
recv(sockfd,buff,127,0); //客户端接收服务器发送过来的数据
printf("buff=%s\n",buff);
close(sockfd);
exit(0);
}
运行有先后顺序,先运行服务器端,在运行客户端。
七、实验改进
7.1 服务器端
一旦有客户端连接成功,便会一直建立连接,循环收发数据!
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
int sockfd = socket_init();
if (sockfd == -1)
{
printf("create socket failed\n");
exit(1);
}
while( 1 )
{
struct sockaddr_in caddr;
int len = sizeof(caddr);
int c = accept(sockfd,(struct sockaddr*)&caddr,&len);//阻塞
if (c < 0 )
{
continue;
}
//上面一旦有客户端连接成功,便会进行下面的循环数据收发
/****改动之处:一直建立连接,可以循环收发数据****/
while ( 1 )
{
char buff[128] = {0};
int n = recv(c,buff,127,0); //可能会阻塞
if(n<=0) //n等于0说明对方关闭,n小于0说明出错了
{
break; //对方关闭后,不需要进行通信了
}
printf("recv(c=%d)=%s\n",c,buff);
send(c,"ok",2,0);
}
close(c); //服务器端也应该关闭与该客户端进行通信
}
}
//封装创建套接字并绑定,进行监听的函数
int socket_init()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);//tcp
if (sockfd == -1)
{
exit(1);
}
struct sockaddr_in saddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET; //地址族 ipv4
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
int res = bind(sockfd, (struct sockaddr*)&saddr, sizeof(saddr));
if (res == -1)
{
printf("bind err\n");
exit(1);
}
if (listen(sockfd, 5) == -1)
{
exit(1);
}
return sockfd;
}
7.2 客户端
客户端连接成功,便会一直建立连接,循环收发数据!
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
//创建套接字
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if (sockfd == -1 )
{
exit(1);
}
//连接服务器
struct sockaddr_in saddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);//ser 6000
saddr.sin_addr.s_addr = inet_addr("43.138.164.79");
int res = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if (res == -1 )
{
printf("connect err\n");
exit(1);
}
printf("连接成功!\n");
//****客户端可以循环进行发送***/
while(1)
{
char buff[128] = {0};
printf("input:\n");
fgets(buff,128,stdin);
if (strncmp(buff, "end", 3) == 0)
{
break;
}
send(sockfd,buff,strlen(buff)-1,0);
memset(buff,0,128);
recv(sockfd,buff,127,0);
printf("buff=%s\n",buff);
}
close(sockfd);
exit(0);
}
至此,已经讲解完毕!篇幅较长,慢慢消化,以上就是全部内容!请务必掌握,创作不易,欢迎大家点赞加关注评论,您的支持是我前进最大的动力!下期再见!