Socket理论
套接字(Socket)概念
套接字(socket)是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。
它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:连接使用的协议,本地主机的IP地址,本地进程的协议端口,远地主机的IP地址,远地进程的协议端口。
应用层通过传输层进行数据通信时,TCP会遇到同时为多个应用程序进程提供并发服务的问题。
多个TCP连接或多个应用程序进程可能需要通过同一个 TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了套接字(Socket)接口。
应用层可以和传输层通过Socket接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。
建立socket连接
建立Socket连接至少需要一对套接字,其中一个运行于客户端
,称为ClientSocket
,另一个运行于服务器端
,称为ServerSocket
。
套接字之间的连接过程分为三个步骤:服务器监听,客户端请求,连接确认
。
服务器监听
:服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态,等待客户端的连接请求。
客户端请求
:指客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。
连接确认
:当服务器端套接字监听到或者说接收到客户端套接字的连接请求时,就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户 端,一旦客户端确认了此描述,双方就正式建立连接。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。
Socket连接与TCP连接
创建Socket连接时,可以指定使用的传输层协议,Socket可以支持不同的传输层协议(TCP或UDP),当使用TCP协议进行连接时,该Socket连接就是一个TCP连接。
Socket连接与HTTP连接
由于通常情况下Socket连接就是TCP
连接,因此Socket连接一旦建立,通信双方即可开始相互发送数据内容,直到双方连接断开。
但在实际网络应用中,客户端到服务器之间的通信往往需要穿越多个中间节点,例如路由器、网关、防火墙等,大部分防火墙默认会关闭长时间处于非活跃状态的连接而导致 Socket 连接断连
,因此需要通过轮询告诉网络,该连接处于活跃状态。
而HTTP连接使用的是“请求—响应”
的方式,不仅在请求时需要先建立连接,而且需要客户端向服务器发出请求后,服务器端才能回复数据。很多情况下,需要服务器端主动向客户端
推送数据,保持客户端与服务器数据的实时与同步。此时若双方建立的是Socket连接
,服务器就可以直接将数据传送给客户端;若双方建立的是HTTP连接
,则服务器需要等到客户端发送一次请求后才能将数据传回给客户端.
因此,客户端定时向服务器端发送连接请求,不仅可以保持在线,同时也是在“询问”服务器是否有新的数据,如果有就将数据传给客户端。
TCP三次握手
服务器监听到连接请求( 服务端启动之后就在不断地监听连接请求),即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态; 客户端收到服务器的SYN K ,ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。
当客户端调用connect时,
第一次握手
建立连接。客户端发送连接请求报文段,将SYN位置为1,Sequence Number为K;然后,客户端SYN_SEND ,connect进入阻塞状态,等待服务器的确认;
第二次握手
服务器监听到连接请求( 服务端启动之后就在不断地监听连接请求) ,服务器收到SYN报文段。服务器收到客户端的SYN 报文段,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态,服务器进入SYN_RECV状态;
(*需要对这个SYN报文段进行确认,设置Acknowledgment Number为K+1(Sequence Number+1);同时,自己自己还要发送SYN请求信息,将SYN位置为1,Sequence Number为y;服务器端将上述所有信息放到一个报文段(即SYN+ACK报文段)中,一并发送给客户端*)
第三次握手
客户端收到服务器的SYN+ACK报文段。
这时connect返回,并对SYN K进行确认,然后将Acknowledgment Number设置为K+1发送,服务器收到ACK K+1时,accept返回。
客户端和服务器端都进入ESTABLISHED状态,完成TCP三次握手。
为什么要进行三次握手?
第一次握手
是客户端向服务端发消息,询问你有没有接收消息的能力?确保消息能准确发送出去,告诉服务端我有写的能力(客户端能发消息到服务端);第二次握手
是指服务端向客户端回消息,标明我收到了消息并且能给你反馈,也就是服务端有读和写的能力(读到客户端消息,并服务端也发消息给我客户端);第三次握手
是客户端给服务端发消息确认建立连接,是告诉服务端我不仅有写的能力,而且我也有读的能力,咱们可以放心通讯了(客户端也能收到服务端消息确认连接)。
这个三次握手发生在socket的那几个函数中呢?请看下图:
四次挥手
第一次分手
主机1(可以使客户端,也可以是服务器端),设置Sequence Number,向主机2发送一个FIN报文段;此时,主机1进入FIN_WAIT_1状态;这表示主机1没有数据要发送给主机2了;
第二次分手
主机2收到了主机1发送的FIN报文段,向主机1回一个ACK报文段,Acknowledgment Number为Sequence Number加1;主机1进入FIN_WAIT_2状态;主机2告诉主机1,我“同意”你的关闭请求;
第三次分手
主机2向主机1发送FIN报文段,请求关闭连接,同时主机2进入LAST_ACK状态;
第四次分手
主机1收到主机2发送的FIN报文段,向主机2发送ACK报文段,然后主机1进入TIME_WAIT状态;主机2收到主机1的ACK报文段以后,就关闭连接;此时,主机1等待2 MSL后依然没有收到回复,则证明Server端已正常关闭,那好,主机1也可以关闭连接了。
为什么要四次分手
TCP协议是一种面向连接的、可靠的、基于字节流的运输层通信协议。TCP是全双工模式,这就意味着,当主机1发出FIN报文段时,只是表示主机1已经没有数据要发送了,主机1告诉主机2,它的数据已经全部发送完毕了;但是,这个时候主机1还是可以接受来自主机2的数据;当主机2返回ACK报文段时,表示它已经知道主机1没有数据发送了,但是主机2还是可以发送数据到主机1的;当主机2也发送了FIN报文段时,这个时候就表示主机2也没有数据要发送了,就会告诉主机1,我也没有数据要发送了,之后彼此就会愉快的中断这次TCP连接。
socket方式
- (void)initScoket
{
//初始化
//每次连接前,先断开连接
if (_clientScoket != 0) {
[self disConnect];
_clientScoket = 0;
}
//创建客户端socket
_clientScoket = CreateClinetSocket();
//服务器Ip
const char * server_ip="127.0.0.1";
//服务器端口
short server_port=6969;
//等于0说明连接失败
if (ConnectionToServer(_clientScoket,server_ip, server_port)==0) {
printf("Connect to server error\n");
return ;
}
//走到这说明连接成功
printf("Connect to server ok\n");
}
static int CreateClinetSocket()
{
int ClinetSocket = 0;
//创建一个socket,返回值为Int。(注scoket其实就是Int类型)
//第一个参数addressFamily IPv4(AF_INET) 或 IPv6(AF_INET6)。
//第二个参数 type 表示 socket 的类型,通常是流stream(SOCK_STREAM) 或数据报文datagram(SOCK_DGRAM)
//第三个参数 protocol 参数通常设置为0,以便让系统自动为选择我们合适的协议,对于 stream socket 来说会是 TCP 协议(IPPROTO_TCP),而对于 datagram来说会是 UDP 协议(IPPROTO_UDP)。
ClinetSocket = socket(AF_INET, SOCK_STREAM, 0);
return ClinetSocket;
}
static int ConnectionToServer(int client_socket,const char * server_ip,unsigned short port)
{
//生成一个sockaddr_in类型结构体
struct sockaddr_in sAddr={0};
sAddr.sin_len=sizeof(sAddr);
//设置IPv4
sAddr.sin_family=AF_INET;
//inet_aton是一个改进的方法来将一个字符串IP地址转换为一个32位的网络序列IP地址
//如果这个函数成功,函数的返回值非零,如果输入地址不正确则会返回零。
inet_aton(server_ip, &sAddr.sin_addr);
//htons是将整型变量从主机字节顺序转变成网络字节顺序,赋值端口号
sAddr.sin_port=htons(port);
//用scoket和服务端地址,发起连接。
//客户端向特定网络地址的服务器发送连接请求,连接成功返回0,失败返回 -1。
//注意:该接口调用会阻塞当前线程,直到服务器返回。
if (connect(client_socket, (struct sockaddr *)&sAddr, sizeof(sAddr))==0) {
return client_socket;
}
return 0;
}
#pragma mark - 对外逻辑
- (void)connect
{
[self initScoket];
}
- (void)disConnect
{
//关闭连接
close(self.clientScoket);
}
//发送消息
- (void)sendMsg:(NSString *)msg
{
const char *send_Message = [msg UTF8String];
send(self.clientScoket,send_Message,strlen(send_Message)+1,0);
}
//收取服务端发送的消息
- (void)recieveAction{
while (1) {
char recv_Message[1024] = {0};
recv(self.clientScoket, recv_Message, sizeof(recv_Message), 0);
printf("%s\n",recv_Message);
}
}
#pragma mark - 新线程来接收消息
- (void)pullMsg
{
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(recieveAction) object:nil];
[thread start];
}
CocoaAsyncSocket 实现
- (void)initSocket
{
gcdSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
}
#pragma mark - 对外的一些接口
static NSString * Khost = @"127.0.0.1";
static const uint16_t Kport = 6969;
//建立连接
- (BOOL)connect
{
return [gcdSocket connectToHost:Khost onPort:Kport error:nil];
}
//断开连接
- (void)disConnect
{
[gcdSocket disconnect];
}
//发送消息
- (void)sendMsg:(NSString *)msg
{
NSData *data = [msg dataUsingEncoding:NSUTF8StringEncoding];
//第二个参数,请求超时时间
[gcdSocket writeData:data withTimeout:-1 tag:110];
}
#pragma mark - GCDAsyncSocketDelegate
//连接成功调用
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
{
NSLog(@"连接成功,host:%@,port:%d",host,port);
[self checkPingPong:110];
//心跳写在这...
}
//断开连接的时候调用
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(nullable NSError *)err
{
NSLog(@"断开连接,host:%@,port:%d",sock.localHost,sock.localPort);
//断线重连写在这...
if (err) {
NSLog(@"连接失败");
}else{
NSLog(@"正常断开");
}
// if ([sock.userData isEqualToString:[NSString stringWithFormat:@"%d",SOCKET_CONNECT_SERVER]])
// {
// //服务器掉线 重新连接
// [self connectToServerWithCommand:@"battery"];
// }else{
// return;
// }
}
//写的回调
- (void)socket:(GCDAsyncSocket*)sock didWriteDataWithTag:(long)tag
{
NSLog(@"写的回调,tag:%ld",tag);
//判断是否成功发送,如果没收到响应,则说明连接断了,则想办法重连
[self checkPingPong:tag];
}
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
NSString *msg = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"收到消息:%@",msg);
[self pullTheMsg:tag];
}
//监听最新的消息
- (void)pullTheMsg:(long)tag
{
//貌似是分段读数据的方法
// [gcdSocket readDataToData:[GCDAsyncSocket CRLFData] withTimeout:10 maxLength:50000 tag:110];
//监听读数据的代理,只能监听10秒,10秒过后调用代理方法 -1永远监听,不超时,但是只收一次消息,
//所以每次接受到消息还得调用一次
[gcdSocket readDataWithTimeout:-1 tag:110];
}
//用Pingpong机制来看是否有反馈
- (void)checkPingPong:(long)tag
{
//pingpong设置为3秒,如果3秒内没得到反馈就会自动断开连接
[gcdSocket readDataWithTimeout:3 tag:110];
}
SocketRocket(WebSocket)
心跳监听
xmppFrameWork: XMPPAutoPing 和 XMPPPing两个类: 心跳监听类;
数据粘包处理
//https://www.jb51.net/article/105278.html
/*
while (_readBuf.length >= 10)//因为头部固定10个字节,数据长度至少要大于10个字节,我们才能得到完整的消息描述信息
{
NSData *head = [_readBuf subdataWithRange:NSMakeRange(0, 10)];//取得头部数据
NSData *lengthData = [head subdataWithRange:NSMakeRange(6, 4)];//取得长度数据
NSInteger length = [[[NSString alloc] initWithData:lengthData encoding:NSUTF8StringEncoding] integerValue];//得出内容长度
NSInteger complateDataLength = length + 10;//算出一个包完整的长度(内容长度+头长度)
if (_readBuf.length >= complateDataLength)//如果缓存中数据够一个整包的长度
{
NSData *data = [_readBuf subdataWithRange:NSMakeRange(0, complateDataLength)];//截取一个包的长度(处理粘包)
[self handleTcpResponseData:data];//处理包数据
//从缓存中截掉处理完的数据,继续循环
_readBuf = [NSMutableData dataWithData:[_readBuf subdataWithRange:NSMakeRange(complateDataLength, _readBuf.length - complateDataLength)]];
}
else//如果缓存中的数据长度不够一个包的长度,则包不完整(处理半包,继续读取)
{
[_socket readDataWithTimeout:-1 buffer:_readBuf bufferOffset:_readBuf.length tag:0];//继续读取数据
return;
}
}
//缓存中数据都处理完了,继续读取新数据
[_socket readDataWithTimeout:-1 buffer:_readBuf bufferOffset:_readBuf.length tag:0];//继续读取数据
END
GCDAsyncSocket类的使用、心跳、粘包处理
阳分析VPN包
TCP建立连接为什么需要三次握手
iOS Socket 心跳
网络层之IP协议
http协议的底层实现
Question
http底层既然是基于socket抽象层,http 经过socket抽象的TCP连接
http 短链接基于TCP链接,socket长链接 TCP/UDP ,
http和 socket 基本属于一个级别,对TCP/UDP的一层封装。
http属于应用层,
socket在应用和传输之间。
TCP/UDP 传输层。
ip协议:对应与网络层 ,IP是一个载体,TCP,UDP,数据都以IP数据格式传输。四次挥手断开问题
如果网络问题断开会咋样Socket Scoket?