赞
踩
关键词:【C/C++】【网络编程】【多线程】【套接字】【UDP】
学习C++网络编程多线程编程的目的:
Q:计算机之间如何通信?
算机之间的通信约定为一种使用socket(套接字)的方式,比如: Web 服务器和浏览器,浏览器获取用户输入的URL,向服务器发起请求,服务器分析接收到的URL,将对应的网页内容返回给浏览器,浏览器再经过解析和渲染,就将文字、图片、视频等元素呈现给用户;
Q:计算机如何知道通讯数据的目的地?
常用IP地址来确定计算机的位置(并不是地理位置,而是网络位置);比如,127.0.0.1 是本机地址;当要通信时,只是将 IP 地址封装到要发送的数据包中,交给路由器去处理。路由器有路由表和路由选择算法,找到目标计算机后将数据包传递给它,完成一次单向通信;
正好来区分下集线器、网桥、交换机、路由器和网关:
为了扩大主机间的通信距离,防止信号在传输过程中的衰减,加入中继器作为通信中介,主要功能:对接收到的信号进行再生整形放大,以扩大网络的传输距离,同时把所有节点集中在以它为中心的节点上。它工作于OSI参考模型第一层,即“物理层”;
采用CSMA/CD(载波侦听多路访问/冲突检测检测协议)访问方式。
CSMA/CD
CSMA/CD(Carrier Sense Multiple Access with Collision Detection,载波侦听多路访问/冲突检测协议),早期主要是以太网络中数据传输方式,广泛应用于以太网中。
载波侦听(Carrier Sense),意思是网络上各个工作站在发送数据前,都要确认总线上有没有数据传输。若有数据传输(称总线为忙),则不发送数据;若无数据传输(称总线为空),立即发送准备好的数据。
多路访问(Multiple Access),意思是网络上所有工作站收发数据,共同使用同一条总线,且发送数据是广播式。
“冲突检测”是指发送结点在发出信息帧的同时,还必须监听媒体,判断是否发生冲突(同一时刻,有无其他结点也在发送信息帧)。CSMA/CD的标准为IEEE802.3或者ISO8802/3。
集线器实际就是一种多端口的中继器。集线器一般有4、8、16、24、32等数量接口,通过这些接口,集线器便能为相应数量的电脑完成“中继”功能;由于它在网络中处于一种“中心”位置,因此集线器也叫做“Hub”;
集线器的工作原理:对于这个8个端口的集线器,共连接了8台电脑。假如1要将一条信息发送给8,当1的网卡将信息通过双绞线送到集线器上时,集线器会进行“广播”,即将信息同时发送给8个端口,各个终端通过验证数据包头的地址信息来确定是否接收,如果目的地是自己就接受,否则不理睬;
HUB 集线器就是一种共享设备,HUB本身不能识别目的地址,也就是说,在这种工作方式下,同一时刻网络上只能传输一组数据帧的通讯,如果发生碰撞还得重试,这种方式就是共享网络带宽;
集线器属于纯硬件网络底层设备,不具备交换机所具有的MAC地址表,所以它发送数据时都是没有针对性的,而是采用广播方式发送。它和双绞线等传输介质一样,是一种不需任何软件支持或只需很少管理软件管理的硬件设备。它被广泛应用到各种场合。
特点:
优点包括:
缺点包括:
为了减少广播风暴,网桥诞生;
Hub无脑广播,而网桥能从发来的数据包中提取MAC信息,并且根据MAC信息对数据包进行有目的的转发,而不采用广播的方式,这样就能减少广播风暴的出现;
网桥用于隔离冲突域,就是通过检查数据包中的地址将两个网段隔离开,比如A发给D的数据不会经过网桥传到另一边,而A发给E的数据就会经过网桥传输过去;一个网段故障也不会对另一网段产生影响,切性能更好(每个端口有独立的交换信道,而Hub共享同一总线);
网桥除了可以扩展网络的物理连接范围外,还可以对MAC 地址进行分区,隔离不同物理网段之间的碰撞(也就是隔离“冲突域”)。集线器和中继器都是物理层设备,而网桥则是链路层设备;所以,Bridge是聪明的Hub;
上图是用一个网桥连接的两个网络,网桥的A端口连接A子网,B端口连接B子网,为什么网桥知道哪些数据包该转发,哪些包不该转发呢?那是因为它有两个表A和B,当有数据包进入端口A时,网桥从数据包中提取出源MAC地址和目的MAC地址。一开始的时候,表A和表B都是空的,没有一条记录,这时,网桥会把数据包转发给B网络,并且在表A中增加一条MAC地址(把源MAC地址记录表中),说明这个MAC地址的机器是A子网的,同理,当B子网发送数据包到B端口时,网桥也会记录源MAC地址到B表;
当网桥工作一段时候后,表A基本上记录了A子网所有的机器的MAC地址,表B同理,当再有一个数据包从A子网发送给网桥时,网桥会先看看数据包的目的MAC地址是属于A子网还是B子网的,如果从A表中找到对应则,抛弃该包,如果不是,则转发给B子网,然后检查源MAC地址,是否在表中已经存在,如果不存在,在表A中增加一条记录。
网桥优点:
网桥缺点:
交换机既有集线器一样的集中连接功能,同时它又具有网桥的数据交换功能。所以可认为交换机是带有交换功能的集线器,或者说交换机是多端口的网桥。外形上,集线器与交换机产品没什么太大区别。
工作原理与网桥类似,总结如下:
特点:
交换机VS网桥
网桥只有2个输入/出端口,而交换机有8个。一开始的时候(那时候只有HUB这种设备),由于硬件水平不是很发达,人们为了提高局域网效率,减少广播风暴的出现,他们生产了网桥(一个只有两个输入/出端口的链路层设备,这时的网桥已经是个比较先进的设备),然后他们把一个局域网一分为二,中间用网桥连接,这样A发给BCD的数据就不会再广播到EFGH了(网桥发现如果数据包不是转发给下面这个子网的,它会自动丢弃此包),只有从A发到EFGH的数据包才能通过网桥,到达另外一个子网(网桥发现如果数据包是转发给下面这个子网的,它才会把包转发给这个子网)。这样一来,非必要的传输减少了,整个网络的效率也随之提高可不少!
人们发现网桥真是个好东西呀,随着硬件发展,出现了4个,8个端口的链路层设备,这就是交换机,由于交换机可以使得网络更安全(很少使用广播,仅在未知目的MAC 地址时采用广播),网络效率更高(数据有目的的传输),交换机渐渐替代了HUB,成为组建局域网的重要设备。
具有多个交换端口
数据转发效率更高
更强的MAC地址自动学习能力
网桥通常只有两个端口,仅可以连接两个由集线器集中连接的物理网段,所以它的MAC 地址自动学习功能仅限于它的两个端口与对应的物理网段的映射。这样就造成了,一个网桥端口要与多个源主机MAC 地址之间的映射,也就是一对多映射关系。而交换机上的端口多数是直接连接主机的,所以在映射表中基本上都是一个源主机MAC 地址与一个交换端口间的一对一映射。一对一的映射查找起来明显比一对多的映射效率要高,所以交换机在数据转发效率要高于网桥。另外,交换机的缓存通常比网桥的要大,所以交换机中可以保存的MAC 地址与端口映射表较多,更适用于较大网络。
交换机VS集线器
集线器工作在第一层(物理层),而交换机至少是工作在第二层,更高级的交换机可以工作在第三层(网络层)、第四层(传输层)和第七层(应用层),对应也就有三层交换机、四层交换机、七层交换机等之说了。一般我们说的就是二层交换机。
集线器的数据传输方式是多次复制方式的广播传输,而交换机的数据传输是有目的的,数据只对目的节点发送,只是在自己的MAC 地址表中找不到的情况下第一次使用以FF-FFFF-FF-FF-FF 作为MAC 地址的“泛洪”广播方式传输。所以,交换机在数据传输效率和信道利用率方面要远高于集线器,集线器更容易产生“广播风暴”。
随交换机产品价格的日益下降,集线器市场日益痿缩,不过,在特定的场合,集线器以其低延迟的特点可以用更低的投入带来更高的效率。交换机不可能完全代替集线器。
路由器是用于连接多个逻辑上分开的网络,所谓逻辑网络是代表一个单独的网络或者一个子网。当数据从一个子网传输到另一个子网时,可通过路由器来完成。因此,路由器具有判断网络地址和选择路径的功能,它能在多网络互联环境中,建立灵活的连接,可用完全不同的数据分组和介质访问方法连接各种子网,路由器只接受源站或其他路由器的信息,属网络层的一种互联设备。它不关心各子网使用的硬件设备,但要求运行与网络层协议相一致的软件。
一般来说,在路由过程中,信息至少会经过一个或多个中间节点。通常,人们会把路由和交换进行对比,这主要是因为在普通用户看来两者所实现的功能是完全一样的。其实,路由和交换之间的主要区别就是交换发生在OSI参考模型的第二层(数据链路层),而路由发生在第三层,即网络层。这一区别决定了路由和交换在移动信息的过程中需要使用不同的控制信息,所以两者实现各自功能的方式是不同的。路由器通过路由决定数据的转发。转发策略称为路由选择,这也是路由器名称的由来。
路由器的主要工作就是为经过路由器的每个数据帧寻找一条最佳传输路径,并将该数据有效地传送到目的站点。 路由器的基本功能是,把数据(IP 报文)传送到正确的网络,细分则包括:
路由器构成了 Internet 的骨架。它的处理速度是网络通信的主要瓶颈之一,它的可靠性则直接影响着网络互连的质量。因此Internet 研究领域中,路由器技术始终处于核心地位。
工作站A需要向工作站B传送信息(并假定工作站B的IP地址为120.0.5),它们之间需要通过多个路由器的接力传递,路由器的分布如图2所示。 工作过程如下所示:
事实上,路由器除了这一功能外,还具有网络流量控制功能。有的路由器仅支持单一协议,但大部分路由器可以支持多种协议的传输,即多协议路由器。由于每一种协议都有自己的规则,要在一个路由器中完成多种协议的算法,势必会降低路由器的性能。因此,我们以为,支持多协议的路由器性能相对较低。
近年来出现了交换路由器产品,从本质上来说它不是什么新技术,而是为了提高通信能力,把交换机的原理组合到路由器中,使数据传输能力更快、更好。
优点包括
缺点包括:
路由器VS交换机
传统的交换机只能分割冲突域,不能分割广播域;而路由器可以分割广播域。由交换机连接的网段仍属于同一个广播域,广播数据包会在交换机连接的所有网段上传播,在某些情况下会导致通信拥挤和安全漏洞。连接到路由器上的网段会被分配成不同的广播域,广播数据不会穿过路由器。虽然第三层以上交换机具有VLAN功能,也可以分割广播域,但是各子广播域之间是不能通信交流的,它们之间的交流仍然需要路由器。
路由器(路由器的安装和配置)提供了防火墙的服务,它仅仅转发特定地址的数据包,不传送不支持路由协议的数据包传送和未知目标网络数据包的传送,从而可以防止广播风暴。
最初的的交换机是工作在OSI/RM开放体系结构的数据链路层,也就是第二层,而路由器一开始就设计工作在OSI模型的网络层。由于交换机工作在OSI的第二层(数据链路层),所以它的工作原理比较简单,而路由器工作在OSI的第三层(网络层),可以得到更多的协议信息,路由器可以做出更加智能的转发决策。
交换机是利用物理地址或者说MAC地址来确定转发数据的目的地址。而路由器则是利用不同网络的ID号(即IP地址)来确定数据转发的地址。IP地址是在软件中实现的,描述的是设备所在的网络,有时这些第三层的地址也称为协议地址或者网络地址。MAC地址通常是硬件自带的,由网卡生产商来分配的,而且已经固化到了网卡中去,一般来说是不可更改的。而IP地址则通常由网络管理员或系统自动分配。
网关(Gateway)又称网间连接器、协议转换器。网关在网络层以上实现网络互连,是最复杂的网络互连设备,仅用于两个高层协议不同的网络互连,网关既可以用于广域网互连,也可以用于局域网互连。
网关是用于连接网络层之上执行不同协议的子网,组成异构的互连网,网关能实现异构设备之间的通信,对不同的传输层、会话层、表示层、应用层协议进行翻译和变换。网关具有对不兼容的高层协议进行转换的功能。当连接两个完全不同结构的网络时,必须使用网关。网关工作在OSI模型的最高层应用层。网关的主要功能:把一种协议变成另一种协议,把一种数据格式变成另一种数据格式,把一种速率变成另一种速率,以求两者的统一。
从根本上说,网关不能完全归为一种网络硬件。用概括性的术语来讲,它们应该是能够连接不同网络的软件和硬件的结合产品。特别地,它们可以使用不同的格式、通信协议或结构连接起两个系统。网关实际上通过重新封装信息以使它们能被另一个系统读取。为了完成这项任务,网关必须能运行在OSI 模型的几个层上。网关必须同应用通信,建立和管理会话,传输已经编码的数据,并解析逻辑和物理地址数据。
‘网关’一个大概念,不具体特指一类产品,只要连接两个不同的网络的设备都可以叫网关;而‘路由器’么一般特指能够实现路由寻找和转发的特定类产品,路由器很显然能够实现网关的功能。
默认网关
默认网关事实上不是一个产品而是一个网络层的概念,PC本身不具备路由寻址能力,所以PC要把所有的IP包发送到一个默认的中转地址上面进行转发,也就是默认网关。这个网关可以在路由器上,可以在三层交换机上,可以在防火墙上,可以在服务器上,所以和物理的设备无关;
网关VS路由器
网关和路由器最大的区别是是否连接相似的网络。如果连接相似的网络,则称为路由器。而连接不相似的网络,称为网关。相似的网络和不相似的网络有两种不同的含义。逻辑层面:相似的网络:如果都是互联网上的两个网络,我们称为相似的网络。不相似的网络:如果一个是私网,一个是公网。我们称为不相似的网络。物理层面:相似的网络:都是以太网或者同一种介质的网络。不相似的网络:一边是以太,一边是SDH或者ATM等。
有了 IP 地址,虽然可以找到目标计算机,但仍然不能进行通信。一台计算机可以同时提供多种网络服务,例如Web服务、FTP服务(文件传输服务)、SMTP服务(邮箱服务)等,仅有 IP 地址,计算机虽然可以正确接收到数据包,但是却不知道要将数据包交给哪个网络程序来处理,所以通信失败;
为了区分不同的网络程序,计算机会为每个网络程序分配一个独一无二的端口号(Port Number),例如,Web服务的端口号是 80,FTP 服务的端口号是 21,SMTP 服务的端口号是 25;
端口(Port)是一个虚拟的、逻辑上的概念。可以将端口理解为一道门,数据通过这道门流入流出,每道门有不同的编号,就是端口号;
协议(Protocol)就是网络通信的约定,通信的双方必须都遵守才能正常收发数据。协议有很多种,例如 TCP、UDP、IP 等,通信的双方必须使用同一协议才能通信。协议是一种规范,由计算机组织制定,规定了很多细节,例如,如何建立连接,如何相互识别等;
所谓协议族(Protocol Family),就是一组协议(多个协议)的统称。最常用的是 TCP/IP 协议族,它包含了 TCP、IP、UDP、Telnet、FTP、SMTP 等上百个互为关联的协议,由于 TCP、IP 是两种常用的底层协议,所以把它们统称为 TCP/IP 协议族;
计算机之间有很多数据传输方式,各有优缺点,常用的有两种:SOCK_STREAM 和 SOCK_DGRAM。
QQ 视频聊天和语音聊天就使用 SOCK_DGRAM 传输数据,因为首先要保证通信的效率,尽量减小延迟,而数据的正确性是次要的,即使丢失很小的一部分数据,视频和音频也可以正常解析,最多出现噪点或杂音,不会对通信质量有实质的影响。
所以,IP地址和端口能够在广袤的互联网中定位到要通信的程序,协议和数据传输方式规定了如何传输数据,有了这些,两台计算机就可以通信了。
基本框架图:
“一切都是文件”的思想极大地简化了程序员的理解和操作,使得对硬件设备的处理就像普通文件一样。所有在Linux中创建的文件都有一个int类型的编号,称为文件描述符(File Descriptor)。使用文件时,只要知道文件描述符就可以。例如,stdin的描述符为0,stdout的描述符为1;
在Linux中,socket也被认为是文件的一种,和普通文件的操作没有区别,所以在网络数据传输过程中自然可以使用与文件I/O相关的函数。可以认为,两台计算机之间的通信,实际上是两个socket文件的相互读写;
文件描述符在windows下被称为句柄;
函数原型:int socket(int af,int type,int protocol);
函数原型:int bind(int sock,struct sockaddr *addr,socklen_t addrlen);
sock 为 socket 文件描述符,addr 为 sockaddr 结构体变量的指针,addrlen 为 addr 变量的大小,可由 sizeof 计算得出;
注意 bind 中要将第二个参数原来的类型(sockaddr_in)转换成 struct sockaddr 类型!
sockaddr_in结构体:
sin_family 和 socket() 的第一个参数含义相同,为地址族;
sin_port 为端口号;uint16_t的长度为两个字节,理论上端口号的取值范围为 0 到65536,但 0 到 1023 的端口一般由系统分配给特定的服务程序,例如Web服务的端口号为 80,FTP 服务的端口号为 21,所以我们的程序要尽星在 1024到65536 之间分配端口号。
sin_addr 是 struct in_addr 结构体类型的变量
struct in_addr{
in_addr_t s_addr; //32位的ip地址
}
in_addr_t 在头文件<netinet/in.h>
中定义,等价于 unsigned long ,长度4个字节,即需要把字符串类型的IP地址转换为长整数类型,使用inet_addr()
进行转换;
unsigned long ip = inet_addr("127.0.0.1")
Q:为什么要是用 sockaddr_in 结构体而不用 sockaddr 呢?反正后面要转换的
A:
因为 sockaddr更加通用;sockaddr和sockaddr_in的长度都是16字节,前者只是将IP地址和端口号合并成一个成员sa_data了;要想给sa_data复制,必须同时知名ip地址和端口号,比如127.0.0.1:80
,但并没有相关函数做到字符串->ip:端口的功能,难以给sockaddr类型的变量复制,所以用sockaddr_in代替;二者结构体长度相同,强制转换类型也不会丢失精度和多余的字节;
通过 listen() 函数可以让套接字进入被动监听状态;
函数原型:int listen(int sock, int backlog);
sock为需要进入监听状态的套接字,backlog为请求队列的最大长度。所谓被动监听,是指当没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户端请求时,套接字才会被“唤醒”来响应请求;
当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。这个缓冲区,就称为请求队列(RequestQueue)。
缓冲区的长度(能存放多少个客户端请求可以通过 listen() 函数的 backlog 参数指定,但究竟为多少并没有什么标准,可以根据你的需求来定,并发量小的话可以是10或者20。如果将backlog 的值设置为SOMAXCONN,就由系统来决定请求队列长度,这个值一般比较大,可能是几百,或者更多。当请求队列满时,就不再接收新的请求,对于Linux,客户端会收到ECONNREFUSED 错误,对于Windows,客户端会收到WSAECONNREFUSED 错误。
当套接字处于监听状态时,可以通过 accept() 函数来接收客户端请求;
函数原型 int accept(int sock,struct sockaddr *addr,socklen_t *addrlen);
它的参数与listen()和connect()是相同的: sock为服务器端套接字,addr为 sockaddr_in结构体变量,addrlen为参数addr 的长度,可由sizeof()求得;
accept() 返回一个新的套接字来和客户端通信,addr保存了客户端的IP地址和端口号,而sock是服务器端的套接字;后面和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字;
listen() 只是让套接字进入监听状态,并没有真正接收客户端请求,listen 后面的代码会继续执行,直到遇到accept();accept()会阻塞程序执行(后面代码不能被执行),直到有新的请求到来;
注意,linux使用read和write接受和发送数据使用recv和send亦可,因为linux不区分套接字和普通文件而windows必须用recv和send;
connect() 函数用来建立连接:
函数原型: int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen);
里面的参数和bind()函数类似;
!socket缓冲区主要解决数据的传递问题
每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。write()和send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情;
TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取;
I/O缓冲区特性可整理如下:
对于TCP套接字(默认情况下),当使用 write()/send() 发送数据时:
首先会检查缓冲区,如果缓冲区的可用空间长度小于要发送的数据,那么 write()/send() 会被阻塞(暂停执行),直到缓冲区中的数据被发送到目标机器,腾出足够的空间,才唤醒 write()/send() 函数继续写入数据;
如果TCP协议正在向网络发送数据,那么输出缓冲区会被锁定,不允许写入,write()/send() 也会被阻塞,直到数据发送完毕缓冲区解锁,write()/send() 才会被唤醒;
如果要写入的数据大于缓冲区的最大长度,那么将分批写入;
直到所有数据被写入缓冲区 write()/send() 才能返回;
当使用 read()/recv() 读取数据时:
首先会检查缓冲区,如果缓冲区中有数据,那么就读取,否则函数会被阻塞,直到网络上有数据到来。
如果要读取的数据长度小于缓冲区中的数据长度,那么就不能一次性将缓冲区中的所有数据读出,剩余数据将不断积压,直到有 read()/recv() 函数再次读取。
直到读取到数据后 read()/recv() 函数才会返回,否则就一直被阻塞。
这就是TCP套接字的阻塞模式。所谓阻塞,就是上一步动作没有完成,下一步动作将暂停,直到上一步动作完成后才能继续,以保持同步性。
数据的接收和发送是无关的,read()/recv() 函数不管数据发送了多少次,都会尽可能多的接收数据。也就是说,read()/recv() 和 write()/send() 的执行次数可能不同。
例如,write()/send() 重复执行三次,每次都发送字符串"abc",那么目标机器上的 read()/recv() 可能分三次接收,每次都接收"abc";也可能分两次接收,第一次接收"abcab",第二次接收"cabc";也可能一次就接收到字符串"abcabcabc"。
假设我们希望客户端每次发送一位学生的学号,让服务器端返回该学生的姓名、住址、成绩等信息,这时候可能就会出现问题,服务器端不能区分学生的学号。例如第一次发送 1,第二次发送 3,服务器可能当成 13 来处理,返回的信息显然是错误的。
这就是数据的“粘包”问题,客户端发送的多个数据包被当做一个数据包接收。也称数据的无边界性,read()/recv() 函数不知道数据包的开始或结束标志(实际上也没有任何开始或结束标志),只把它们当做连续的数据流来处理。
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的通信协议,数据在传输前要建立连接,传输完毕后还要断开连接。
客户端在收发数据前要使用 connect() 函数和服务器建立连接。建立连接的目的是保证IP地址、端口、物理链路等正确无误,为数据的传输开辟通道。
TCP建立连接时要传输三个数据包,俗称三次握手(Three-way Handshaking)。可以形象的比喻为下面的对话:
[Shake 1] 套接字A:“你好,套接字B,我这里有数据要传送给你,建立连接吧。”
[Shake 2] 套接字B:“好的,我这边已准备就绪。”
[Shake 3] 套接字A:“谢谢你受理我的请求。”
使用 connect() 建立连接时,客户端和服务器端会相互发送三个数据包客户端调用 socket() 函数创建套接字后,因为没有建立连接,所以套接字处于CLOSED状态;服务器端调用 listen() 函数后,套接字进入LISTEN状态,开始监听客户端请求;
这个时候,客户端开始发起请求:
当客户端调用 connect() 函数后,TCP协议会组建一个数据包,并设置 SYN 标志位,表示该数据包是用来建立同步连接的。同时生成一个随机数字 1000,填充“序号(Seq)”字段,表示该数据包的序号。完成这些工作,开始向服务器端发送数据包,客户端就进入了SYN-SEND状态;
服务器端收到数据包,检测到已经设置了 SYN 标志位,就知道这是客户端发来的建立连接的“请求包”。服务器端也会组建一个数据包,并设置 SYN 和 ACK 标志位,SYN 表示该数据包用来建立连接,ACK 用来确认收到了刚才客户端发送的数据包。服务器生成一个随机数 2000,填充“序号(Seq)”字段。2000 和客户端数据包没有关系。服务器将客户端数据包序号(1000)加1,得到1001,并用这个数字填充“确认号(Ack)”字段。服务器将数据包发出,进入SYN-RECV状态;
客户端收到数据包,检测到已经设置了 SYN 和 ACK 标志位,就知道这是服务器发来的“确认包”。客户端会检测“确认号(Ack)”字段,看它的值是否为 1000+1,如果是就说明连接建立成功。接下来,客户端会继续组建数据包,并设置 ACK 标志位,表示客户端正确接收了服务器发来的“确认包”。同时,将刚才服务器发来的数据包序号(2000)加1,得到 2001,并用这个数字来填充“确认号(Ack)”字段。客户端将数据包发出,进入ESTABLISED状态,表示连接已经成功建立;
服务器端收到数据包,检测到已经设置了 ACK 标志位,就知道这是客户端发来的“确认包”。服务器会检测“确认号(Ack)”字段,看它的值是否为 2000+1,如果是就说明连接建立成功,服务器进入ESTABLISED状态。
至此,客户端和服务器都进入了ESTABLISED状态,连接建立成功,接下来就可以收发数据了;
三次握手的关键是要确认对方收到了自己的数据包,这个目标就是通过“确认号(Ack)”字段实现的。计算机会记录下自己发送的数据包序号 Seq,待收到对方的数据包后,检测“确认号(Ack)”字段,看Ack = Seq + 1是否成立,如果成立说明对方正确收到了自己的数据包;
向主机B传递200字节的过程。首先,主机A通过1个数据包发送100个字节的数据,数据包的 Seq 号设置为 1200。主机B为了确认这一点,向主机A发送 ACK 包,并将 Ack 号设置为 1301。为了保证数据准确到达,目标机器在收到数据包(包括SYN包、FIN包、普通数据包等)包后必须立即回传ACK包,这样发送方才能确认数据传输成功。
此时 Ack 号为 1301 而不是 1201,原因在于 Ack 号的增量为传输的数据字节数。假设每次 Ack 号不加传输的字节数,这样虽然可以确认数据包的传输,但无法明确100字节全部正确传递还是丢失了一部分,比如只传递了80字节。
因此按如下的公式确认 Ack 号:Ack号 = Seq号 + 传递的字节数 + 1 表示你下次要发送序列号以Ack开头的数据了;
!当传输发生错误时,如何处理?
上图表示通过 Seq 1301 数据包向主机B传递100字节的数据,但中间发生了错误,主机B未收到。经过一段时间后,主机A仍未收到对于 Seq 1301 的ACK确认,因此尝试重传数据。为了完成数据包的重传,TCP套接字每次发送数据包时都会启动定时器,如果在一定时间内没有收到目标机器传回的 ACK 包,那么定时器超时,数据包会重传。ACK 包丢失的情况,一样会重传;
重传超时时间(RTO, Retransmission Time Out)这个值太大了会导致不必要的等待,太小会导致不必要的重传,理论上最好是网络 RTT 时间,但又受制于网络距离与瞬态时延变化,所以实际上使用自适应的动态算法(例如 Jacobson 算法和 Karn 算法等)来确定超时时间;
往返时间(RTT,Round-Trip Time)表示从发送端发送数据开始,到发送端收到来自接收端的 ACK 确认包(接收端收到数据后便立即确认),总共经历的时延;
重传次数TCP数据包重传次数根据系统设置的不同而有所区别。有些系统,一个数据包只会被重传3次,如果重传3次后还未收到该数据包的 ACK 确认,就不再尝试重传。但有些要求很高的业务系统,会不断地重传丢失的数据包,以尽最大可能保证业务数据的正常交互;
建立连接非常重要,它是数据正确传输的前提;断开连接同样重要,它让计算机释放不再使用的资源。如果连接不能正常断开,不仅会造成数据传输错误,还会导致套接字不能关闭,持续占用资源,如果并发量高,服务器压力堪忧。建立连接需要三次握手,断开连接需要四次握手,可以形象的比喻为下面的对话:
[Shake 1] 套接字A:“任务处理完毕,我希望断开连接。”
[Shake 2] 套接字B:“哦,是吗?请稍等,我准备一下。”
等待片刻后……
[Shake 3] 套接字B:“我准备好了,可以断开连接了。”
[Shake 4] 套接字A:“好的,谢谢合作。”
建立连接后,客户端和服务器都处于ESTABLISED状态。这时,客户端发起断开连接的请求:
客户端调用 close() 函数后,向服务器发送 FIN 数据包,进入FIN_WAIT_1状态。FIN 是 Finish 的缩写,表示完成任务需要断开连接。
服务器收到数据包后,检测到设置了 FIN 标志位,知道要断开连接,于是向客户端发送“确认包”,进入CLOSE_WAIT状态。
注意:服务器收到请求后并不是立即断开连接,而是先向客户端发送“确认包”,告诉它我知道了,我需要准备一下才能断开连接。
客户端收到“确认包”后进入FIN_WAIT_2状态,等待服务器准备完毕后再次发送数据包。
等待片刻后,服务器准备完毕,可以断开连接,于是再主动向客户端发送 FIN 包,告诉它我准备好了,断开连接吧。然后进入LAST_ACK状态。
客户端收到服务器的 FIN 包后,再向服务器发送 ACK 包,告诉它你断开连接吧。然后进入TIME_WAIT状态。
服务器收到客户端的 ACK 包后,就断开连接,关闭套接字,进入CLOSED状态。
Q:客户端最后一次发送 ACK包后进入 TIME_WAIT 状态,而不是直接进入 CLOSED 状态关闭连接,这是为什么呢?
A:
TCP 是面向连接的传输方式,必须保证数据能够正确到达目标机器,不能丢失或出错,而网络是不稳定的,随时可能会毁坏数据,所以机器A每次向机器B发送数据包后,都要求机器B”确认“,回传ACK包,告诉机器A我收到了,这样机器A才能知道数据传送成功了。如果机器B没有回传ACK包,机器A会重新发送,直到机器B回传ACK包;
客户端最后一次向服务器回传ACK包时,有可能会因为网络问题导致服务器收不到,服务器会再次发送 FIN 包,如果这时客户端完全关闭了连接,那么服务器无论如何也收不到ACK包了,所以客户端需要等待片刻、确认对方收到ACK包后才能进入CLOSED状态。那么,要等待多久呢?
数据包在网络中是有生存时间的,超过这个时间还未到达目标主机就会被丢弃,并通知源主机。这称为报文最大生存时间**(**MSL,Maximum Segment Lifetime)。TIME_WAIT 要等待 2MSL 才会进入 CLOSED 状态。ACK 包到达服务器需要 MSL 时间,服务器重传 FIN 包也需要 MSL 时间,2MSL 是数据包往返的最大时间,如果 2MSL 后还未收到服务器重传的 FIN 包,就说明服务器已经收到了 ACK 包。
调用 close()/closesocket() 函数意味着完全断开连接,即不能发送数据也不能接收数据;主机A发送完数据后,单方面调用 close()/closesocket() 断开连接,之后主机A、B都不能再接受对方传输的数据。实际上,是完全无法调用与数据收发有关的函数。
一般情况下这不会有问题,但有些特殊时刻,需要只断开一条数据传输通道,而保留另一条。使用 shutdown() 函数可以达到这个目的:
int shutdown(int sock, int howto); //Linux
sock 为需要断开的套接字,howto 为断开方式。
howto 在 Linux 下有以下取值:
SHUT_RD:断开输入流。套接字无法接收数据(即使输入缓冲区收到数据也被抹去),无法调用输入相关函数。
SHUT_WR:断开输出流。套接字无法发送数据,但如果输出缓冲区中还有未传输的数据,则将传递到目标主机。
SHUT_RDWR:同时断开 I/O 流。相当于分两次调用 shutdown(),其中一次以 SHUT_RD 为参数,另一次以 SHUT_WR 为参数。
Q:close()/closesocket()和shutdown()的区别:
A:
确切地说,close() / closesocket() 用来关闭套接字,将套接字描述符(或句柄)从内存清除,之后再也不能使用该套接字,与C语言中的fclose() 类似。应用程序关闭套接字后,与该套接字相关的连接和缓存也失去了意义,TCP协议会自动触发关闭连接的操作;而shutdown() 用来关闭连接,而不是套接字,不管调用多少次 shutdown(),套接字依然存在,直到调用 close() / closesocket() 将套接字从内存清除。
调用 close()/closesocket() 关闭套接字时,或调用 shutdown() 关闭输出流时,都会向对方发送 FIN 包。FIN 包表示数据传输完毕,计算机收到 FIN 包就知道不会再有数据传送过来了。默认情况下,close()/closesocket() 会立即向网络中发送FIN包,不管输出缓冲区中是否还有数据,而shutdown() 会等输出缓冲区中的数据传输完毕再发送FIN包。也就意味着,调用 close()/closesocket() 将丢失输出缓冲区中的数据,而调用 shutdown() 不会。
功能:client 从 server 下载一个文件并保存到本地;
文件大小不确定,有可能比缓冲区大很多,调用一次 write()/send() 函数不能完成文件内容的发送,接收数据时也会遇到同样的情况。要解决这个问题,可以使用 while 循环;
对于 Server 端的代码,当读取到文件末尾,fread() 会返回 0,结束循环。
对于 Client 端代码,有一个关键的问题,就是文件传输完毕后让 recv() 返回 0,结束 while循环。
注意:读取完缓冲区中的数据 recv() 并不会返回 0,而是被阻塞,直到缓冲区中再次有数据。
Client 端如何判断文件接收完毕,也就是上面提到的问题——何时结束 while 循环?
最简单的结束 while 循环的方法当然是文件接收完毕后让 recv() 函数返回 0,那么,如何让 recv() 返回 0 呢?recv() 返回 0 的唯一时机就是收到FIN包时。
FIN 包表示数据传输完毕,计算机收到 FIN 包后就知道对方不会再向自己传输数据,当调用 read()/recv() 函数时,如果缓冲区中没有数据,就会返回 0,表示读到了”socket文件的末尾“。
这里我们调用 shutdown() 来发送FIN包:server 端直接调用 close()/closesocket() 会使输出缓冲区中的数据失效,文件内容很有可能没有传输完毕连接就断开了,而调用 shutdown() 会等待输出缓冲区中的数据传输完毕。代码:http://c.biancheng.net/cpp/html/3045.html
CPU向内存保存数据的方式有两种:
大端序(Big Endian):高位字节存放到低位地址(高位字节在前);
小端序(Little Endian):高位字节存放到高位地址(低位字节在前);
不同CPU保存和解析数据的方式不同(主流的Intel系列CPU为小端序),小端序系统和大端序系统通信时会发生数据解析错误。因此在发送数据前,要将数据转换为统一的格式——网络字节序(Network Byte Order)。网络字节序统一为大端序。
主机A先把数据转换成大端序再进行网络传输,主机B收到数据后先转换为自己的格式再解析。serv_addr.sin_port = htons(1234); //端口号
htons() 用来将当前主机字节序转换为网络字节序,其中h代表主机(host)字节序,n代表网络(network)字节序,s代表short,htons 是 h、to、n、s 的组合,可以理解为”将short型数据从当前主机字节序转换为网络字节序“;
常见的网络字节转换函数有:
htons():host to network short,将short类型数据从主机字节序转换为网络字节序。
ntohs():network to host short,将short类型数据从网络字节序转换为主机字节序。
htonl():host to network long,将long类型数据从主机字节序转换为网络字节序。
ntohl():network to host long,将long类型数据从网络字节序转换为主机字节序。
另外需要说明的是,sockaddr_in 中保存IP地址的成员为32位整数,而我们熟悉的是点分十进制表示法,例如 127.0.0.1,它是一个字符串,因此为了分配IP地址,需要将字符串转换为4字节整数。inet_addr() 除了将字符串转换为32位整数,同时还进行网络字节序转换;
UDP不像TCP,无需在连接状态下交换数据,因此基于UDP的服务器端和客户端也无需经过连接过程。也就是说,不必调用 listen() 和 accept() 函数。UDP中只有创建套接字的过程和数据交换的过程。
TCP中,套接字是一对一的关系。如要向10个客户端提供服务,那么除了负责监听的套接字外,还需要创建10套接字。但在UDP中,不管是服务器端还是客户端都只需要1个套接字。之前解释UDP原理的时候举了邮寄包裹的例子,负责邮寄包裹的快递公司可以比喻为UDP套接字,只要有1个快递公司,就可以通过它向任意地址邮寄包裹。同样,只需1个UDP套接字就可以向任意主机传送数据。
创建好TCP套接字后,传输数据时无需再添加地址信息,因为TCP套接字将保持与对方套接字的连接。换言之,TCP套接字知道目标地址信息。但UDP套接字不会保持连接状态,每次传输数据都要添加目标地址信息,这相当于在邮寄包裹前填写收件人地址。
发送数据使用 sendto() 函数:
ssize_t sendto(int sock, void *buf, size_t nbytes, int flags, struct sockaddr *to, socklen_t addrlen); //Linux
UDP 发送函数 sendto() 与TCP发送函数 write()/send() 的最大区别在于,sendto() 函数需要向他传递目标地址信息;
接收数据使用 recvfrom() 函数:
ssize_t recvfrom(int sock, void *buf, size_t nbytes, int flags, struct sockadr *from, socklen_t *addrlen); //Linux
Linux下基于UDP的回声C/S通讯程序
Server.c
Client.c
在网上查资料,都是这样描述 TCP面向连接,可靠,数据流 。UDP无连接,不可靠,数据报。但是实际使用的时候就会有很多疑惑了,比如我们做一个聊天软件 客户登陆我们的服务器,我们到底是使用哪一种协议呢 是使用TCP和客户端保持常连接,还是使用UDP这种无连接,数据传输不可靠 还是使用TCP在和客户端交换一次数据后就断开连接 需要的时候再连接。
这是3种情况。这几种情况针对于需要服务器转发的消息,需要客户端之间点对点传输的情况除外
使用TCP协议和客户端保持常连接(长连接) 从客户登陆到客户离线,在客户端与服务端之间一直保持着一个TCP连接,两者之间可以随时相互通信,信息的传输是可靠的,这是我们最想看到的方式。但是我们不禁要问了 一台服务器能保持多少个TCP连接呢,这是我一直困惑的,前端时间遇到一个有些经验的程序员 按我的理解,使用这种常连接方式的C/S软件,如果服务器是一台普通的计算机 一般情况下,500多个TCP连接之后就会开始出现问题。
对于真正的服务器,我们可以想象它是通过一个网络设备充当网关(类似于路由器,硬件防火墙)连接到公网,服务器连接到网关,服务器对外服务的端口都和网关有映射关系。所以我们客户端知道服务器公网IP和服务端口 可以直接发起TCP连接请求。所有的数据交换都会经过服务器的网关 而这个网关,根据我的了解 它能并发处理500到10W个连接不等,这主要取决于这个硬件防火墙的质量,直接和价格相关。
我们一般家里或者寝室使用的路由器都能同时处理几百个连接,使数据几乎是没多少延迟就通过网关了,而服务器的网关我们可以想象 再差也能支持几千个并发连接吧,而在服务器网关背后有可能不止一台服务器,对于大规模的即时通讯软件来说 服务器网关背后肯定是服务器集群,服务端也是分布式的 服务端运行在这个服务器集群的多台服务器中,由多台服务器来对客户端发来的数据进行协同地均衡负载处理,以支持海量用户 处理完成之后通过网关把结果发给客户。
我们可以想象,像腾讯QQ之类的大规模即时通讯软件,经常性的是几千万用户同时在线 如果都采用常连接的方式。岂不是要服务器的硬件防火墙监控数千万个连接了,就算分布式服务端能承受这么多用户 网关也受不了,而且有理由相信服务器也受不了 。
所以对于大规模即时通讯,尤其是用户数量众多 肯定不能用TCP常连接的方式,这种方式只适合于小规模的即时通讯,如局域网,公司内部的即时通讯等 对于大规模的,用户数量众多的C/S软件 应当采用UDP协议进行数据传输,网关就不停收发数据包就可以了;
使用TCP协议和客户端进行短命连接,用了就关 比如客户登陆请求好友列表,我们就和他建立TCP连接发给他好友列表,然后关掉连接 当用户要给另外一个客户发信心我们再建立连接,数据传完我们又关掉连接 这种方式,无疑服务器可以承载更多的用户登陆。但是缺点也是非常明显的 一般情况下我们接收的数据都不大,每次发一点点消息都要建立连接 TCP本来就比较消耗网络资源,这样毫无规律的断断连连 连连断断,加上本来这种方式就有较高的延迟 也不适合大规模的即时通讯;
使用UDP协议与客户端无连接 UDP无连接,不可靠,数据报。无连接,代表了它快速,资源消耗小 接着我们要看“不可靠”,这个不可靠它究竟不可靠到了什么程度呢?事实上,在Internet上传输的UDP包从A发送给B 它完整地到达几率一般情况下还是相当之高的。我们开发一个多用户的即时通讯软件 采用UDP传输消息的时候,报文被划分成包 一个UDP包究竟是多大?经过我的了解一个UDP用户包最大大小是64KB 根据网络状况,实际在传输包的时候可能把包划成若干个分片 一个分片的最大大小是1640B 可以保存好几百个汉字。我们在使用QQ的时候应该也注意到了,我们给好友发送信息的时候都有字数上限的,我的理解这是为了让传输的信息不至于太长而分成多个包,保证所有信息装在一个包里,要么完整发送,要么传输失败。我们客户端向服务端发送的消息 一般来说可以定义一个结构体 这个结构体包含指令 消息正文等要素(见之前写的进程间通信基础知识)我们都知道这个结构体其实很简单,只有几个成员 包括正文,对于一般的文字消息,或者请求 更新好友列表的消息对象被序列化后也很小,一个UDP包被完全可以装下这个序列化后的对象。到这里我们再来看看UDP协议的“不可靠”,“数据报”。
UDP协议提供数据报机制传输信息 如果报文比较长,比如一个文件,一个图片 要被划分成若干个数据包,由于对于一般的文字消息和其它指令都是比较小的 它们会被放在一个包里面,由于UDP是无连接的 不可靠的,如果发生丢包,不会重传 所以不能保证数据包能完整并准确地到达目的地,但是对于我们的即时通讯软件来说 一般的聊天信息比较小,比如我们给一个好友发送一条聊天信息“今天我很高兴”,会不会服务器转发的时候只收到“今天我很高”,再传给好友的时候变成了“今天”,答案是不会发生这种情况的 UDP虽然描述是不可靠,不过依然在数据包中包含了头信息描述了包的大小等信息,在包进行转发的过程中 如果数据不完整,是会被网络设备发现的,比如中途一个转发这个包的路由器发现了一个不完整的UDP包会直接丢弃,如果是TCP 当有包被丢弃了会进行重传,对于UDP 包在传输过程中由于数据的缺失被丢弃不会进行重传 我们顶多就是一条信息发送失败了,而这种概率一般情况下是非常低的 。
经过以上的一些思考,我得出了这样的结论 大规模即时通讯应当采用UDP协议进行常规的通信,TCP可以作为一种辅助手段经过一些学习,思考和实践 我得出了心目中的大规模即时通讯软件的总体架构:
以UDP协议作为主要数据传输协议
服务端使用一个数据库保存信息(分布式数据库)
服务端是是分布式的,通过异步多线程技术,集群服务等 通过服务器集群共同运行服务端,对外进行海量信息处理(转发,暂存,广播消息)
客户端也属于分布式应用程序,具有一些服务端的功能 在进行语言,视频,文件传输的时候,可由服务端协调 在两个客户端直接进行点对点通信
这样的总体架构能够保证大规模即时通讯,更需要做的地方就是对数据的查询优化服务端的性能优化等。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。