socket通信原理
socket又被叫做套接字,它就像连接到两端的插座孔一样,通过建立管道,将两个不同的进程之间的端口进行连接,保持双方通信,它实际上是对TCP/IP协议的上层封装.socket通信,实质上也就是基于TCP协议可靠连接进行通信。在理解socket通信之前,我们有必要来复习一下TCP的三次握手与挥手.
TCP概述:
TCP协议的三次握手与四次握手流程分析,基本上所有的网络通信协议都是由TCP/IP协议衍生出来,大都基于 IP + Port的寻址方式,只是在报文格式上和警报协议上,数据响应的方式上做了相关的扩展和约定。
-
TCP的报文格式:
序号:Seq序号,占32位,用来标示从TCP源端向目的端发送的字节流,发起发送数据时对此进行标记。标记当前数据传输的唯一标示符,类似UUID
确认序列号:Ack序号,占32位,只有Ack标志位1的,确认序号字段才有效,Ack=Seq+1
标志位共6个,及URG,ACK,PSH,RST,SYN,FIN
(A)URG:紧急指针(urgent pointer)有效。
(B)ACK:确认序号有效。
(C)PSH:接收方应该尽快将这个报文交给应用层。
(D)RST:重置连接。
(E)SYN:发起一个新连接。
(F)FIN:释放一个连接。
需要注意的是:
(A)不要将确认序号Ack与标志位中的ACK搞混了。
(B)确认方Ack=发起方Seq+1,两端配对。 -
三次握手:
所谓的三次握手(Three-Way Hankshake)及建立TCP连接,就是指建立一个TCP连接时,需要客户端和服务为端总共发送3个包以确认连接建立。在socket编程中,这一过程主要由客户端connect来触发,整个流程如下图所示.
(A):第一次握手:Client将标志位SYN置为1(代表发起一个新的连接),随机生成一个值seq=J,并将该数据包发送给server,Client进入SYN_SENT状态(请求建立连接状态),等待服务器确认。
(B):第二次握手:Server收到数据包根据传过来的标志位SYN=1(客户端请求与服务端建立连接),知道是Client请求建立连接,Server(发送数据包)将标志位SYN(服务端请求与客户端建立连接)和ACK置为1(确认客户端序号,表示该序号对应的数据服务端有收到了,既确认序号有效),并将确认序号ack=j+1(既为客户端发送的序号+1),同时产生一个随机的seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态(发起建立连接等待状态)。
(C):第三次握手:Client收到确认后,检查确服务器发送过来的确认序列号 ack是否为J+1,ACK是否为1(是否为自己上次传入的的序号seq=J 当次所发出的数据包,服务是否已经确认有收到),如果✅则将标志位ACK置为1(表示客户端知道了这个数据是服务器发出的),同理再生成一个确认序列号,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确,则连接成功。
至此:Client和Server进入ESTABLISHED状态(建立连接状态),完成三次握手,Client和Server之间就可以开始传输数据了。注意点:
seq:序列号,主要为了表明数据包发送的唯一性,作为数据包发送的唯一标示。
ack:确认序列号,标示收到 序列号seq对应的数据。定义为seq+1.
ACK:确认标志符,标示某一方收到了数据包并且成功解析。确认成功 ACK=1.
ACK=1与ack=1+seq的序列号 标示确认 某一次特定序列号seq的报文有成功收到,ACK=1 和ack=1+seq作为响应数据告诉发起方有成功收到了该次的响应。 -
SYN攻击:
在三次握手过程中,server发送SYN-ACK之后,收到Client的ACK之前的TCP连接成为半连接状态(half-open connect),此时Server处于SYN_RCVD状态,当收到的ACK后,Server专区ESTABLISHED状态。
SYN攻击就是 构造虚伪的客户端Client在短时间内伪造大量不存在的IP地址,并不断的向服务器发送SYN包(请求建立连接的包),Server回复确认包,并等待Client的确认,由于源地址是不存在的,因此,Server需要不断重发至超时,这些伪造的SYN包将占用确认的连接的队列,导致正常的SYN请求因为队列已满耳杯丢弃,从而引起网络堵塞甚至系统瘫痪,SYN攻击是一种典型的DDOS攻击,检测SYN攻击的方法非常简单,当Server有大半连接状态的源IP地址是随机的,则可以断定是遭遇到了SYN攻击了,使用如下命令可以让之现行:
#netstat-nap|grep SYN_RECV
DDoS::Distributed Denial of Service 分布式的拒绝服务,多个计算机联合起来对一个或多个目标发起攻击,从而成倍地提高拒绝服务攻击的威力 -
四次挥手:
Four-Way Wavehand:既终止TCP的连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开,在socket编程中,这一过程由客户端或者服务端任何一方之行close
由于TCP连接时是全双工的,因此,每个方向上都必须要尽兴单独的关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向上的连接(主动关闭),接受方收到传来的FIN信号就知道发送方关闭了数据流动,它不会再发数据我了。 为了能彻底与发送发断绝联系,此时接受方也向发送方发送了FIN信号。于是两端都被彻底关闭了,连接彻底的断开了。
(A):第一次挥手:Clinet发送一个FIN标识符信号(告诉服务器我这边的传输通道要关闭了),Clinet进入了FIN_WAIT_1状态.
(B):第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号为收到的序号M+1(同上面三次握手中SYN相同,一个FIN占用一个序号),Server此时进入CLOSE_WAIT状态。此时完成了Client->sever之间数据传送关闭
(C):第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态(最后确认关闭状态)
(D):第四次挥手:Client收到FIN后,Clinet进入TIME_WAIT状态,接着发送一个ACK给Server,确认序列号为收到的序列号+1,Server进入Closed状态,完成四次挥手。
实际过程中也有可能出现同时发起主动关闭的情况。
简单流程: 我(客户端)要关闭通道,已经确认你这边的通道关闭,我(服务器端)也要关闭通道,已经确认你这边通道关闭。你这边的通道已经处理完成了。
TCP协议的特点:transfrom control protocol,提供面向连接,可靠的字节流服务,当客户端和服务器端彼此交换数据前必须在双方基础上建立一个TCP连接,之后才能传输数据,TCP提供超时重发,丢弃重复数据,数据校验,流量控制,保证数据正确可靠的从一端传向另外一端。
socket常用函数
pragma mark socket函数的使用
/**
1.在Linux中,一切皆文件,除了文本文件,源文件,二进制文件等,一个硬件设备也可以被映射微一个虚拟的文件,成为设备文件.例如, stdin成为标准输入文件,它对应的硬件设备一般是键盘,stdout成为标准的输出文件,它对应的硬件设备一般为显示器。对于所有的文件都可以使用read()函数读取数据,使用write函数写入数据。
2.‘一切皆文件的思想极大的简化了程序源的理解和操作,使得对硬件设备的处理就好像普通的文件一样,所有在Liunx中创建的文件都有一个int类型的编码,成为文件描述符号(File Descriptor),使用文件时,只要知道文件对应的描述符。例如stdin的文件描述符号为0,stdout描述为1.
- 在Liunx中socket也被认为是一种文件,和普通文件的操作没有区别,所以在网络数据传输过程中自然可以使用与文件I/O相关的函数.可以认为两台计算机之间的通信,实际上是两个socket文件的相互读写。
文件描述符也被称为文件举兵(File Handle),但“句柄”主要是Windows中术语。 - 在Liunx下创建socket
在Liunx下使用<sys/socket.h>头文件中的socket()函数来创建套接字。原型为
int socket(int af,int type,int protocol)
a. af为地址族(Adress Family),也就是IP类型,常用的有AF_INET和AF_INET6,AF是'Address Family'的简写。INET是‘Internet’的简写,AF_INET表示IPV4地址,例如127.0.0.1;AF_INET6表示IPV6表示IPV6的地址,例如 1030::C9B4:FF12:48AA:1A2B
也可以称为PF protocol family
b. type为数据传输方式,常用的有SOCK_STREAM和SOCK_DGRAM
c. protocol表示传输协议,常用的有IPPROTO_TCP和IPTOTO_UDP,分别表示TCP传输协议和UDP传输协议。
正如大家所想,一般情况下只要有af和type两个参数就可以创建套接字了,操作系统会自动退演出协议的类型,除非遇到这样的情况,有两种不同的协议都支持同一种地址类型和数据传输类型,如果不明确制定哪中协议,操作系统是没有办法自动推演的。
5.如下实例教程中使用IPV4地址,参数af的职位PF_INET,如果使用SOCKET_STREAM传输数据,那么满足这个两个条件的只有TCP.
//TCP套接字
int tcp_scoket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP)
//UDP套接字
如果使用SOCK_DGRAM传输方式,那么满足这两个条件的协议只有UDP,因此可以这样来调用socket函数
int udp_socket = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP)
//以上两种情况都只有一种协议满足条件,可以将protocol值设置为0,由系统自动推演出应该使用很么样的协议
int tcp_socket = socket(AF_INET, SOCK_STREAM, 0); //创建TCP套接字
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0); //创建UDP套接字
在Windows下创建socket
Windows 下也使用 socket() 函数来创建套接字,原型为:
SOCKET socket(int af, int type, int protocol);
除了返回值类型不同,其他都是相同的。Windows 不把套接字作为普通文件对待,而是返回 SOCKET 类型的句柄。请看下面的例子:
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0); //创建TCP套接字
6.sock()函数涌来创建套接字,然后确定套接字的各种属性,然后服务器端需要用bind()函数将套接字语特定的IP地址和端口绑定起来,只有这样,流经该IP地址和端口的数据才能交给套接字处理.而客户端谣使用connect()函数建立链接。
bind()函数
int bind(int scok,struct sockaddaddr,socklent addlen); //地址结构体的长度
int bind(int sock,const struct sockaddraddr,int addrlen); //Windows
sock为socket的文件描述符,addr为sockaddr结构体变量的指针,addrlen为addr结构体变量的大小,可以有sizeof()来求出
完整代码:
//创建套接字
int serv_sock = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
struct sockaddr_in serv_addr;
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_family = AF_INET; //使用的地址类型为IPV4;
serv_addr.sin_addr.s_addr= inet_addr("127.0.0.1");//具体的ip地址,序列话
serv_addr.sin_port = htons(1234); //端口
bind(serv_sock,(struc sockaddr*)&serv_addr,sizeof(serv_addr));
//sockaddr_in 结构体
struct sockadd_in 的成员变量如下:
sa_family_t sin_family; //地址族(Adress Family),也就是底子类型
unit16_t sin_port; //16位的端口号。
struct in_addr sin_addr; //32位的IP地址
char sin_zero[8] //不使用,一般用0填充
a.sin_family和socket()的第一个参数的含义相同,取值也要保持一致。
b.sin_port为端口号,unit16_t的长度为两个字节,理论上端口号的曲直范围为0~65536,但 0~1023 的端口一般由系统分配给特定的服务程序,例如 Web 服务的端口号为 80,FTP 服务的端口号为 21,所以我们的程序要尽量在 1024~65536 之间分配端口号。
c.端口号需要使用htons()函数进行转换。
sin_addr是struct in_addr结构体类型变量。
d.sin_zero[8] 是多余的8个字节,没有用,一般使用 memset() 函数填充为 0。上面的代码中,先用 memset() 将结构体的全部字节填充为 0,再给前3个成员赋值,剩下的 sin_zero 自然就是 0 了。
e.in_addr_t在头文件<netinet/in.h>中定义,等价于unsigned long,长度为4个字节(32位),夜就是说s_addr是一个证书,而IP地址是一个字符串,所以需要inet_add()函数进行转换.
sockaddr 和 sockaddr_in 的长度相同,都是16字节,只是将IP地址和端口号合并到一起,用一个成员 sa_data 表示。要想给 sa_data 赋值,必须同时指明IP地址和端口号,例如”127.0.0.1:80“,遗憾的是,没有相关函数将这个字符串转换成需要的形式,也就很难给 sockaddr 类型的变量赋值,所以使用 sockaddr_in 来代替。这两个结构体的长度相同,强制转换类型时不会丢失字节,也没有多余的字节。
f.可以任务,sockaddr是一种通用的机构体,可以用来保存多种类型的IP地址和端口号,而sockaddr_in是专门用来保存IPV4的结构体,另外还有sockaddr_in6,用来保存IPV6地址.它的定义如下。
struct sockaddr_in6{
sa_family_t sin6_family; //地址类型,取之为AF_INF6
in_port_t sin6_port; //16位的端口号
unit32_t sin6_flowInfo; //IPV6流信息
struct in6_addr sin6_addr; //具体的IPV6地址
unit32_t sin6_socpe_id; //接口范围ID
}
7. connect()函数
int connect(int sock,struct sockaddr*sev_addr,socklent_t addrlen); Liunx
int connect(SOCKET sock,const struct sockaddr* serv_addr,int addrlen);
8. 对于服务器端的程序,使用blid()绑定套接字后,海需要使用listen()函数让套接字进入被动监听状状态,再掉哟給accept函数,就可以随时响应客户的请求了。
listen()
int listen(int sock,int backlog);
int listen(SOCKET sock,int backlog);
sock为需要进入监听的恶套接字,backlog为请求队列的最大长度。
所谓的被动监听,指当没有客户端请求时,套接字处于睡眠的状态,只有当接受到客户端请求时,套接字才会被唤醒来响应请求。
//请求队列
当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没有办法处理的,只能把它放入缓冲区,待,待当前请求处理完毕后,再从缓冲区中读取出来处理.如果不断有请求进来,他们就按照先后顺序放入缓冲区中排队。这个缓冲区就称为请求队列(Request Queue).
如果将backlog的值设置为SOMAXCONN,那么久由系统来自动决定请求对列的长度,这个值一般为几百,甚至更大.并发量小的话可以是10或者20。
当请求对垒满载的时候就不在接受新的请求,对于Liunx,客户端会受到ECONNREFUSED❌,
9.accept函数
套接字处于监听状态时,可以通过accept()函数来接受客户端的请求,它的原型为:
int accept(int sock,struct sockaddr* addr,socklen_t* addrlen);
SOCKET accept(SOCKET sock,struct sockaddr*addr,int * addrlen);
它的参数与listen和connect()是相同的,sock为服务器端套接字,add为sockaddr_in结构体变量,addrlen为参数addr的长度,可以有sizeof()获取
主要: listen只是让套接字进入监听状态,并没有真正的接受客户的请求,listen()后面的代码会继续执行,知道遇到accept(),accept()会阻塞程序执行,知道
10.Linux下数据的接受和发送
前面我门说过,两台计算机之间的通信相当于两个套字节之间的通信,在服务器端使用write()和read()读取和写入字节,这样就完成了一次通信。
write()原型
ssize_t write(int fd,const void* buf ,size_t nbytes);
fd为要写入文件的描述符,buf为要卸乳的数据的缓冲区的地址,nbytes为要写入的字节数。 size_t 为unsigned int类型, ssize_t前面的s为singed int类型
read()原型为 ssize_t read(int fd,void* buf ,size_t nbytes);
read()函数会从未fd文件中毒曲nbytes个字节并保存到缓冲区buf中,成功返回读取到的字节数。(遇到文件结尾返回则返回0),失败则返回 -1.
11.Windows下的数据接受和发送
从从服务器端发送数据使用send()函数,它的原型为
int send(SOCKET sock,const char* buf,int len, int flags);
sock为需要发送数据的套接字,buf为需要发送数据的缓冲区地址,len为需要发送的字节,flag为s为发送数据时的选项.
在客户端中接受数据使用recv()函数,它的原型为
int recv(SOCKET sock,char*buf,int len, int flags);
12. socket开发中迭代开发.
为了保证服务器端和客户端持续链接。使用while循环,
connect-》send->重置缓冲区
accept->send -》重置缓冲区
构建死循环,不端的进行链接。
13. socket缓冲区
write()和send()并不立即向网络中传输数据,而是先将其写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦数据写入到缓冲区中,函数就可以成功的返回,不管它有没有到达目标及其。也不管他们是合适发送到网络。这些都是TCP协议负责的事情。
TCP协议独立于write()/send()函数,数据有可能刚被写入缓冲区就发送到网络,也可能仔缓冲区中不断积压。多次写入的数据被一次发送到网络,这取决当时的网络情况,当前线程是否空闲等诸多因素,不由程序猿控制。
read()/recv()函数也是从缓冲区中读取数据,而不失直接从网络中获取。
//I/O缓冲区在每个TCP套接字中单独存在
//I/O缓冲区在创建套接字时自动生成
//及时关闭了套接字也会继续传送缓冲区中遗留的数据
//关闭套接字将丢失舒服穿冲去的中数据。
//输入缓冲区中数据默认大小一般为8k,可以通过getsockopt()函数来获取。
unsigned optVal;
int optLen = sizeof(int);
getsockopt(servSock, SOL_SOCKET, SO_SNDBUF, (char*)&optVal, &optLen);
printf("Buffer length: %d\n", optVal);
14. 阻塞模式
对于TCP套接字(默认情况下),当使用write()/send()发送数据时:
a.首先会检查缓冲区,如果缓冲区的可用空间长度小于要发送的数据长度。那么write()/send()会被阻塞(暂停执行),知道缓冲区的数据被发送到目标及其上,腾出足够的空间,才唤醒write()/send()函数继续写入数据。
b.如果TCP协议正在向网络发送数据,那么输出缓冲区会被锁定,不允许写入.write()/send()也会被阻塞,知道数据发送完毕缓冲区解锁,write()/send()函数才会被唤醒。
c.1) 首先会检查缓冲区,如果缓冲区中有数据,那么就读取,否则函数会被阻塞,直到网络上有数据到来。
2) 如果要读取的数据长度小于缓冲区中的数据长度,那么就不能一次性将缓冲区中的所有数据读出,剩余数据将不断积压,直到有 read()/recv() 函数再次读取。
3) 直到读取到数据后 read()/recv() 函数才会返回,否则就一直被阻塞。
这就是TCP套接字的阻塞模式。所谓阻塞,就是上一步动作没有完成,下一步动作将暂停,直到上一步动作完成后才能继续,以保持同步性。
*/
网友评论