iOS开发--socket通讯

tech2022-10-17  109

Socket通信

🍉概念TCPUDPsocket 基本原理连接建立步骤参考文章

🍉

概念

在移动开发中,我们在很频繁地和后台接口进行数据通讯,通常是http请求,http是一种无状态的协议,无状态是指Web浏览器和Web服务器之间不需要建立持久的连接,这意味着当一个客户端向服务器端发出请求,然后Web服务器返回响应(response),连接就被关闭了,在服务器端不保留连接的有关信息,http遵循请求(Request)/应答(Response)模型。Web浏览器向Web服务器发送请求,Web服务器处理请求并返回适当的应答。所有http连接都被构造成一套请求和应答, 服务器和客户端是如何建立http请求连接过程的呢?答案是通过建立TCP连接,即http是基于TCP三次握手进行连接的。 一次完整的HTTP请求过程从TCP三次握手建立连接成功后开始,客户端按照指定的格式开始向服务端发送HTTP请求,服务端接收请求后,解析HTTP请求,处理完业务逻辑,最后返回一个HTTP的响应给客户端,HTTP的响应内容同样有标准的格式。无论是什么客户端或者是什么服务端,大家只要按照HTTP的协议标准来实现的话,那么它一定是通用的。

TCP

TCP(传输控制协议) 建⽴连接,形成传输数据的通道 在连接中进⾏⼤数据传输(数据⼤⼩不收限制) 通过三次握⼿完成连接,是可靠协议,安全送达 必须建⽴连接,效率会稍低

UDP

UDP(⽤户数据报协议) 只管发送,不确认对⽅是否接收到 将数据及源和⽬的封装成数据包中,不需要建⽴连接 每个数据报的⼤⼩限制在64K之内 因为⽆需连接,因此是不可靠协议 不需要建⽴连接,速度快 应⽤场景:多媒体教室/⽹络流媒体

socket

Socket就是为⽹络服务提供的⼀种机制 通信的两端都是 Socket ⽹络通信其实就是 Socket 间的通信 数据在两个 Socket 间通过 IO 传输 Socket 是纯C语⾔的,是跨平台的

基本原理

连接建立步骤

客户端的步骤为:   1、创建一个socket,用函数socket();   2、设置socket属性,用函数setsockopt();* 可选   3、绑定IP地址、端口等信息到socket上,用函数bind();* 可选   4、设置要连接的对方的IP地址和端口等属性;   5、连接服务器,用函数connect();   6、收发数据,用函数send()和recv(),或者read()和write();   7、关闭网络连接;

#pragma mark 创建socket连接 - (void)socketConnetAction:(UIButton *)sender { /** 1: 创建socket 参数 domain:协议域,又称协议族(family)。常用的协议族有AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域Socket)、AF_ROUTE等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。 type:指定Socket类型。常用的socket类型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。流式Socket(SOCK_STREAM)是一种面向连接的Socket,针对于面向连接的TCP服务应用。数据报式Socket(SOCK_DGRAM)是一种无连接的Socket,对应于无连接的UDP服务应用。 protocol:指定协议。常用协议有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等,分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。 注意:1.type和protocol不可以随意组合,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当第三个参数为0时,会自动选择第二个参数类型对应的默认协议。 返回值: 如果调用成功就返回新创建的套接字的描述符,如果失败就返回INVALID_SOCKET(Linux下失败返回-1) */ _clinenId = socket(AF_INET, SOCK_STREAM, 0); if (_clinenId == -1) { NSLog(@"创建socket 失败"); return; } //2: 建立连接 /** __uint8_t sin_len; 假如没有这个成员,其所占的一个字节被并入到sin_family成员中 sa_family_t sin_family; 一般来说AF_INET(地址族)PF_INET(协议族) in_port_t sin_port; // 端口 struct in_addr sin_addr; // ip char sin_zero[8]; 没有实际意义,只是为了 跟SOCKADDR结构在内存中对齐 */ struct sockaddr_in socketAddr; socketAddr.sin_family = AF_INET; socketAddr.sin_port = SocketPort; struct in_addr socketIn_addr; socketIn_addr.s_addr = SocketIP; socketAddr.sin_addr = socketIn_addr; /** 参数 参数一:套接字描述符 参数二:指向数据结构sockaddr的指针,其中包括目的端口和IP地址 参数三:参数二sockaddr的长度,可以通过sizeof(struct sockaddr)获得 返回值 成功则返回0,失败返回非0,错误码GetLastError()。 */ int result = connect(_clinenId, (const struct sockaddr *)&socketAddr, sizeof(socketAddr)); if (result != 0) { NSLog(@"连接socket 失败"); return; } [sender setTitle:@"已连接" forState:UIControlStateNormal]; sender.userInteractionEnabled = NO; self.restartId = 0; NSLog(@"连接成功"); dispatch_async(dispatch_get_global_queue(0, 0), ^{ [self recvMsg]; }); } #pragma mark 发送数据 -(void)sendMsgAction:(id)sender { /** 3: 发送消息 s:一个用于标识已连接套接口的描述字。 buf:包含待发送数据的缓冲区。 len:缓冲区中数据的长度。 flags:调用执行方式。 返回值 如果成功,则返回发送的字节数,失败则返回SOCKET_ERROR 一个中文对应 3 个字节!UTF8 编码! */ if (self.sendMsgContent_tf.text.length == 0) { NSLog(@"消息为空,无法发送"); return; } const char *msg = self.sendMsgContent_tf.text.UTF8String; ssize_t sendLen = send(self.clinenId, msg, strlen(msg), 0); NSLog(@"发送了:%ld字节",sendLen); [self showMsg:self.sendMsgContent_tf.text msgType:0]; self.sendMsgContent_tf.text = @""; } #pragma mark 接受数据 - (void)recvMsg { // 4. 接收数据 /** 参数 1> 客户端socket 2> 接收内容缓冲区地址 3> 接收内容缓存区长度 4> 接收方式,0表示阻塞,必须等待服务器返回数据 返回值 如果成功,则返回读入的字节数,失败则返回SOCKET_ERROR */ while (1) { if (self.clinenId) { return; } uint8_t buffer[1024]; ssize_t recvLen = recv(self.clinenId, buffer, sizeof(buffer), 0); NSLog(@"接收到了:%ld字节",recvLen); // 判断如果 0 下面会奔溃 if (recvLen == 0) { self.restartId ++; if (self.restartId > 3) { self.restartId = 0; return; } NSLog(@"此次传输长度为0 如果下次还为0 请检查连接"); continue; } // 接收到的数据转换 NSData *recvData = [NSData dataWithBytes:buffer length:recvLen]; NSString *recvStr = [[NSString alloc] initWithData:recvData encoding:NSUTF8StringEncoding]; NSLog(@"%@",recvStr); self.restartId = 0; dispatch_async(dispatch_get_main_queue(), ^{ [self showMsg:recvStr msgType:1]; }); } } #pragma mark 关闭socket - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ if (self.clinenId) { // 7: 关闭socket连接 int close_result = close(self.clinenId); if (close_result == -1) { NSLog(@"socket 关闭失败"); return; }else{ NSLog(@"socket 关闭成功"); } } }

服务器端的步骤是:   1、创建一个socket,用函数socket();   2、设置socket属性,用函数setsockopt(); * 可选   3、绑定IP地址、端口等信息到socket上,用函数bind();   4、开启监听,用函数listen();   5、接收客户端上来的连接,用函数accept();   6、收发数据,用函数send()和recv(),或者read()和write();   7、关闭网络连接;   8、关闭监听;

#pragma mark - 创建socket建立连接 - (void)socketConnetAction:(UIButton *)sender { // 1: 创建socket self.serverId = socket(AF_INET, SOCK_STREAM, 0); if (self.serverId == -1) { NSLog(@"创建socket 失败"); return; } NSLog(@"创建socket 成功"); struct sockaddr_in socketAddr; socketAddr.sin_family = AF_INET; socketAddr.sin_port = SocketPort; struct in_addr socketIn_addr; socketIn_addr.s_addr = SocketIP; socketAddr.sin_addr = socketIn_addr; bzero(&(socketAddr.sin_zero), 8); // 2: 绑定socket int bind_result = bind(self.serverId, (const struct sockaddr *)&socketAddr, sizeof(socketAddr)); if (bind_result == -1) { NSLog(@"绑定socket 失败"); return; } NSLog(@"绑定socket成功"); // 3: 监听socket int listen_result = listen(self.serverId, kMaxConnectCount); if (listen_result == -1) { NSLog(@"监听失败"); return; } NSLog(@"监听成功"); // 4: 接受客户端的链接 for (int i = 0; i < kMaxConnectCount; i++) { [self acceptClientConnet]; } } #pragma mark - 接受客户端的链接 - (void)acceptClientConnet { // 阻塞线程 dispatch_async(dispatch_get_global_queue(0, 0), ^{ struct sockaddr_in client_address; socklen_t address_len; // accept函数 int client_socket = accept(self.serverId, (struct sockaddr *)&client_address, &address_len); self.client_socket = client_socket; if (client_socket == -1) { NSLog(@"接受 %u 客户端错误",address_len); }else{ NSString *acceptInfo = [NSString stringWithFormat:@"客户端 in,socket:%d",client_socket]; NSLog(@"%@",acceptInfo); [self receiveMsgWithClietnSocket:client_socket]; } }); } - (void)receiveMsgWithClietnSocket:(int)clientSocket{ while (1) { // 5: 接受客户端传来的数据 char buf[1024] = {0}; long iReturn = recv(clientSocket, buf, 1024, 0); if (iReturn>0) { NSLog(@"客户端来消息了"); // 接收到的数据转换 NSData *recvData = [NSData dataWithBytes:buf length:iReturn]; NSString *recvStr = [[NSString alloc] initWithData:recvData encoding:NSUTF8StringEncoding]; NSLog(@"%@",recvStr); }else if (iReturn == -1){ NSLog(@"读取消息失败"); break; }else if (iReturn == 0){ NSLog(@"客户端走了"); close(clientSocket); break; } } } #pragma mark - socket 发送消息 - (void)didClickSendAction:(id)sender { // 6: 发送消息 const char *msg = self.sendMsgContent_tf.text.UTF8String; ssize_t sendLen = send(self.client_socket, msg, strlen(msg), 0); NSLog(@"发送了:%ld字节",sendLen); self.sendMsgContent_tf.text = @""; } #pragma mark - 关闭socket - (IBAction)didClickCloseAction:(id)sender { // 7: 关闭socket连接 int close_result = close(self.client_socket); if (close_result == -1) { NSLog(@"socket 关闭失败"); return; }else{ NSLog(@"socket 关闭成功"); } }

作为TCP/IP传输协议的一种,客户端和服务端通过TCP建立socket连接的内部原理远比上图的步骤复杂,客户端和服务端是通过何种方式来建立连接的呢 ?我们知道客户端要和服务端进行连接需要通过3次握手:

断开连接步骤

断开连接,即tcp的四次挥手步骤:

完整的过程 从图中可以看出,当客户端调用connect时,触发了连接请求,向服务器发送了SYN J包,这时connect进入阻塞状态;服务器监听到连接请求( 服务端启动之后就在不断地监听连接请求),即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态;客户端收到服务器的SYN K ,ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。 客户端或者服务端首先调用close主动关闭连接,这时TCP发送一个FIN M; 另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据; 一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N; 接收到这个FIN的源发送端TCP对它进行确认。 这样每个方向上都有一个FIN和ACK.

参考文章

iOS开发之Socket通信基本原理

最新回复(0)