服务器端和客户端的断开连接的四次挥手为啥不一样??????

任务描述 本关任务: 编写两个程序实现客户端与服务端的通信,clinet.c为客户端,server.c为服务器端服务器端能够读取客户端发送的信息。 相关知识 为了完成本关任务,你需要掌握:1.socket编程,2.数据通信过程。 1、Socket 网络中进程可以通过socket通信,socket起源于Unix,满足“一切皆文件”原理,即操作为“打开open->读写write/read->关闭close”。 ①socket的调用函数主要有: socket()/*创建描述符,设定协议域socket类型*/ bind()/*绑定地址*/ listen()/*监听*/ connect()/*连接*/ read()/*I/O读操作*/ write()/*I/O写操作*/ close()/*断开连接*/ shutdown()/*部分断连*/ ②socket操作主要有三类: 1)三次握手:让客户端与服务端双方都能明确自己对方的收、发能力是正常的。 2)数据传输:socket使用的是TCP连接,为双向传输的对等模式,双方都可以同时向对方发送或接收数据。 3)四次挥手:由于TCP连接是全双工,每个方向都必须单独进行关闭。 2、socket函数 int socket(int domain,int type,int protocol) sockfd=socket(); socket()创建一个socket描述符,标识唯一一个socket domain:协议域->AF_INET、AF_INET6、AF_LOCAL(AF_UNIX)、AF_ROUTE。协议族决定socket地址类型,通信中必须采用对应的地址,AF_INET决定要用ipv4地址(32位)与端口号(16位)组合看,AF_UNIX决定用一个绝对路径作为地址。 type:socket->指定socket类型。常用SOCK_STREAM(TCP)、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET、SOCK_DGRAM(UDP)等 protocol:协议->常用协议IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等,分别对应TCP传输、UDP传输、STCP传输、TIPC传输 注:typeprotocol可用随意组合,SOCKET_STREAMIPPROTO_UDP可用组合。当protocol为0,会自动选择type类型对应默认协议。 socket函数创建一个socket时,返回的socket描述符存在于协议族address family中,没有具体地址,赋值一个地址必须调用bind()或者调用connect()、listen()系统自动随机分配端口。 3、bind函数 bind()函数把一个地址族中特定地址赋值给socket,AF_INET、AF_INET6就是一个ipv4或ipv6地址端口号组合,赋值给socket。 int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen); sockfd:socket描述符,通过socket()创建,唯一标识一个socket。bind()将这个描述符绑定一个名字。 addr:const struct sockaddr*指针,指向sockfd的协议地址。地址结构根据创建socket时的地址协议族同而变化。 addrlen:地址长度 注:①服务器启动时会绑定地址(ip+端口号)。提供服务时,服务端通过地址连接服务器客户端用绑定,系统自动分配端口号自身iP地址组合。所以通常服务端listen前会调用bind(),客户端会调用,在connect()时系统随机生成一个。 ②主机字节序(大端小端模式):同CPU有同字节序类型,这些字节序指整数在内存中保存的顺序。 1)little-endian,低位字节放内存低地址段,高位字节排放内存高地址端。 2)big-endian,高位字节放内存低地址段,低位字节放内存高地址端。 网络字节序:4个字节的32bit值以下次序传输,0-7、8-15、16-23、24-31。大端字节序。由于TCP/IP首部中所有二进制整数在网络中传输时都要求以这种次序,因此又称作网络字节序。字节序,发育一个字节类型数据在内存中存放顺序,一个字节的数据没有顺序问题。 在绑定地址到socket时候,必须将主机字节序转换成网络字节序,能让主机字节序跟网络字节序一样使用big-endian。 4、listen、connetct函数 服务器调用socket()、bind()后就会调用listen()监听socket,如果客户端这时候调用connect()发出连接请求,服务器就会接受这个请求。 int listen(int sockfd,int backlog); int connect(int sockfd,const struct sockaddr *addr,socklen_t addrlen); listen中sockfd为要监听的socket描述符,backlog为相应socket可以排队的最大连接个数。 socket()函数创建的socket是一个主动类型,listen()将socket变为被动类型等待客户连接请求。 connect中sockfd为客户端socket描述符,addr为服务器socket地址,addrlen为socket地址长度。客户端调用connect函数建立与TCP服务器的连接。 5、accept函数 TCP服务器调用socket()、bind()、listen()后,监听socket地址。TCP客户端调用socket()、connect()后,给TCP服务器发送一个连接请求。TCP服务器监听到请求,调用accept()接收请求,连接建立好开始I/O操作。 int accept(int sockfd,struct sockaddr *addr,socklen_t addrlen) sockfd服务器socket描述符 addr返回客户端协议地址 addrlen协议地址长度 accept成功,返回值由内核自动生成的全新描述符,表示已与返回客户的TCP连接。 注:sockfd是服务器的socket描述符,服务器开始调用socket()生成的,即监听socket描述符,accept返回的是已连接socket描述符,一个服务器通常只创建一个监听socket描述符,在服务器生命周期内会一直存在,内核为每个服务器进程接收的客户连接创建一个已连接socket描述符,当服务器完成客户服务,相应已连接socket描述符会被关闭。 6、read、write函数 服务器与客户建立好连接,调用网络I/O进行读写操作,网络中同进程之间通信。 网络I/O操作 read()/write() recv()/send() readv()/writev() recvmsg()/sendmsg() recvfrom/sendto() 7、close、shutdown函数 服务器客户端建立连接后,读写操作完,需要关闭相应的socket描述符。 #include<unistd.h> int close(int fd); sockfd能在作为read或者write的第一个参数。 注:close操作只是将相应的sockfd描述字引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端服务器发送终止连接请求。 shutdown()函数,可以关闭socket一端或者全部。 int shutdown(int _fd,int _how) TCP连接是双向的(可读可写),使用close读写通道都关闭,使用shutdown有三种: ①howto=0,关闭读通道,可以继续写。 ②howto=1,关闭写通道,只可以读。 ③howto=2,关闭读写通道,close一样,全部关闭。 8、三次握手 tcp建立连接进行“三次握手”: ①客户端服务器发送一个SYN x。 ②服务器客户端发送一个SYN y,并对SYN x进行ACK x+1。 ③客户端再向服务器发一个ACK y+1。 ①客户端调用connect触发连接请求,向服务器发送SYN x包,connect进入阻塞状态; ②服务器listen监听到连接请求(收到SYN x),调用accept接受请求,向客户端发送SYN yACK x+1,accept进入阻塞状态; ③客户端收到服务器的SYN yACK x+1,对SYN y确认,connect返回ACK y+1; ④服务器收到ACK y+1,accept返回,三次握手完毕,连接建立。 注:客户端的connect在三次握手的第二次握手返回,服务端的accept在三次握手的第三次握手返回。 9、数据传输 ①建立完连接后,进行数据传输,readwrite。 ②客户端发起给服务端写入,并发送SYN x+1ACK y+1给服务端(xy未发生变化)。 ③服务端发起给客户端读取,并发送ACK x+2。 10、四次挥手客户端应用进程调用close主动关闭连接,客户端TCP发送FIN x+2ACK y+1。 ②服务端接受到FIN x+2,执行被动关闭,对FIN确认。即当服务端收到FIN后停止数据的操作并发出一个ACK x+3客户端表示数据操作结束。 ③一段时间后,服务端应用接收到文件结束符的应用进程调用close关闭服务端socket,发送一个FIN。(表示socket已经关闭) ④客户端接收到FIN,关闭客户端的socket,发送ACK y+2高速服务端。 编程要求及注意事项 根据client.c代码及提示,在右侧编辑器中选中server.c注释处补充代码。 client.c如下: #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #define MAXLINE 4096 int main(int argc, char *argv[]) { int sockfd, n; // sockfd是客户端创建的socket描述字,客户端需要区分监听listenfd连接connfd。n为发送长度 char recvline[MAXLINE], sendline[MAXLINE]; // recvline暂时没用,sendline为即将发送的数据 struct sockaddr_in servaddr; //创建需要连接的服务端地址 if (argc != 2) { printf("usage: ./client <ipaddress>"); exit(0); } //设置运行时候需要输入的格式,执行文件+服务端ip地址 if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { printf("create socket error:%s(errno:%d)\n", strerror(errno), errno); exit(0); } //创建socket描述字,因为客户端,只需要一次连接 memset(&servaddr, 0, sizeof(servaddr)); //先把当前连接的服务端地址全填充0 servaddr.sin_family = AF_INET; //目的服务端地址协议簇为ipv4 servaddr.sin_port = htons(6666); //设置端口号6666 if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) { printf("inet_pton error for %s\n", argv[1]); exit(0); } //将目的服务端的地址,转换成网络地址,必须转换成网络地址!!argv[0]是./client,argv[1]是服务端的地址 if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) { printf("connect socket error:%s(errno:%d)\n", strerror(errno), errno); exit(0); } //创建连接,本机的socket通过服务端地址连接服务端,强转指针格式 printf("send msg to server:\n"); //连接上后输出发送信息到服务端 fgets(sendline, 4096, stdin); //标准输入流中获取需要发送的信息,存储到sendline字符数组中 if (send(sockfd, sendline, strlen(sendline), 0) < 0) { printf("send msg error:%s(errno:%d)\n", strerror(errno), errno); exit(0); } //发送信息,sendline buff中的信息 close(sockfd); //信息发送完关闭自己的sockfd,第一次挥手 exit(0); } server.c如下: #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <unistd.h> #define MAXLINE 4096 #define PORT 6666 // 与客户端端口一致 int main() { int listenfd, connfd; // 监听socket连接socket struct sockaddr_in servaddr; // 服务端地址结构 char buffer[MAXLINE]; // 接收缓冲区 // 1. 创建监听socket if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { printf("Create socket error: %s(errno: %d)\n", strerror(errno), errno); exit(0); } // 2. 配置服务端地址 memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有网络接口 servaddr.sin_port = htons(PORT); // 端口号6666 // 3. 绑定socket到地址 if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1) { printf("Bind socket error: %s(errno: %d)\n", strerror(errno), errno); exit(0); } // 4. 开始监听连接 if (listen(listenfd, 10) == -1) { // 等待队列最大10 printf("Listen socket error: %s(errno: %d)\n", strerror(errno), errno); exit(0); } printf("====== Server running on port %d ======\n", PORT); printf("Waiting for client connection...\n"); // 5. 接受客户端连接 if ((connfd = accept(listenfd, (struct sockaddr*)NULL, NULL)) == -1) { printf("Accept socket error: %s(errno: %d)\n", strerror(errno), errno); } else { printf("Client connected!\n"); } // 6. 读取客户端数据 int n = recv(connfd, buffer, MAXLINE, 0); if (n > 0) { buffer[n] = '\0'; // 确保字符串终止 printf("Received message from client: %s\n", buffer); } else if (n == 0) { printf("Client closed connection\n"); } else { printf("Recv error: %s(errno: %d)\n", strerror(errno), errno); } // 7. 关闭连接 close(connfd); close(listenfd); printf("Server shutdown\n"); return 0; }
最新发布
06-14
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值