网络编程基础

网络基础知识

 一些常见术语的基本概念

网络分类

按照网络的作用范围分类,我们可以将其分为局域网、城域网、广域网3类。

    a) 局域网(Local Area Network,简称LAN)

    局域网通常都是私有的,用于连接一个办公室、建筑物或者校园内的设备,通常范围设定在千米以内。通常情况下一个局域网只会采用一种拓扑结构。

    b) 广域网(Wide Area Network,简称WAN)

    广域网常常横跨多个国家、地区或大洲,所覆盖的范围通常到几千千米,并且被多个机构所拥有。广域网通常利用电信部门提供的分组交换网、卫星通信信道和无线分组交换网,将分布在多个地区的计算机系统互联起来,达到资源共享的目的。
 

网络通信概述
从进程间通信说起:网络域套接字socket,网络通信其实就是位于网络中不同主机上面的2个进程之间的通信。

网络通信的层次
(1)硬件部分:网卡
(2)操作系统底层:网卡驱动
(3)操作系统API:socket接口
(4)应用层:低级(直接基于socket接口编程)
(5)应用层:高级(基于网络通信应用框架库)
(6)应用层:更高级(http、网络控件等)

例如下图就简单的描述了网络通信的层次:

网卡
(1)计算机上网必备硬件设备,CPU靠网卡来连接外部网络
(2)串转并设备
(3)数据帧封包和拆包
(4)网络数据缓存和速率适配


集线器(HUB)
(1)信号中继放大,相当于中继器
(2)组成局域网络,用广播方式工作。
(3)注意集线器是不能用来连接外网的


交换机
(1)包含集线器功能,但更高级
(2)交换机中有地址表,数据包查表后直达目的通信口而不是广播
(3)找不到目的口时广播并学习

路由器
(1)路由器是局域网和外部网络通信的出入口
(2)路由器将整个internet划分成一个个的局域网,却又互相联通。
(3)路由器对内管理子网(局域网),可以在路由器中设置子网的网段,设置有线端口的IP地址,设置dhcp功能等,因此局域网的IP地址是路由器决定的。
(4)路由器对外实现联网,联网方式取决于外部网络(如ADSL拨号上网、宽带帐号、局域网等)。这时候路由器又相当于是更高层级网络的其中一个节点而已。
(5)所以路由器相当于有2个网卡,一个对内做网关、一个对外做节点。
(6)路由器的主要功能是为经过路由器的每个数据包寻找一条最佳路径(路由)并转发出去。其实就是局域网内电脑要发到外网的数据包,和外网回复给局域网内电脑的数据包。
(7)路由器技术是网络中最重要技术,决定了网络的稳定性和速度。


DNS(Domain Name Service 域名服务)
(1)网络世界的门牌号:IP地址
(2)IP地址的缺点:难记、不直观
(3)IP地址的替代品:域名,譬如www.zhulaoshi.org
(4)DNS服务器就是专门提供域名和IP地址之间的转换的服务的,因此域名要购买的
(5)我们访问一个网站的流程是:先使用IP地址(譬如谷歌的DNS服务器IP地址为8.8.8.8)访问DNS服务器(DNS服务器不能是域名,只能是直接的IP地址),查询我们要访问的域名的IP地址,然后再使用该IP地址访问我们真正要访问的网站。这个过程被浏览器封装屏蔽,其中使用的就是DNS协议。
(6)浏览器需要DNS服务,而QQ这样的客户端却不需要(因为QQ软件编程时已经知道了腾讯的服务器的IP地址,因此可以直接IP方式访问服务器)
 


IP地址分类(IPv4)


(1)IP地址实际是一个32位二进制构成,在网络通信数据包中就是32位二进制,而在人机交互中使用点分十进制方式显示 既:0.0.0.0 ~ 255.255.255.255 每一个十进制代表8位二进制。
(2)IP地址中32位实际包含2部分,分别为:网络地址和主机地址。(哪些位表示网络地址是由子网掩码决定的)

子网掩码,用来说明网络地址和主机地址各自占多少位。

要想判断两个ip地址是不是在同一个网段,只需将ip地址与子网掩码做与运算,如果得出的结果一样,则这两个ip地址是同一个子网当中。
(3)由网络地址和主机地址分别占多少位的不同,将IP地址分为5类,最常用的有3类

例如:子网掩码为 255.255.255.0  既前24位数代表网络地址 后8位数代表主机地址


三类IP地址
(1)A类。
(2)B类
(3)C类
(4)127.0.0.0用来做回环测试loopback(就是网卡自己发自己收 进行测试)

对于A类地址来说,默认的子网掩码是255.0.0.0;对于B类地址来说默认的子网掩码是255.255.0.0;对于C类地址来说默认的子网掩码是255.255.255.0。

注意:并不是说所有的子网掩码都是255.0.0.0   255.255.0.0     255.255.255.0


如何判断2个IP地址是否在同一子网内
(1)网络标识 = IP地址 & 子网掩码
(2)2个IP地址的网络标识一样,那么就处于同一网络。

源IP地址:发出数据包的网络的IP地址
目标IP地址:要接收数据包的计算机的IP地址


二进制方式            0xffffffff            0xC0A80166/0x6601A8C0        本质
点分十进制方式        255.255.255.255        192.168.1.102                方便人看的

IP地址 = 网络地址 + 主机地址
网络地址用来表示子网
主机地址是用来表示子网中的具体某一台主机的。


譬如可以8位表示网络,24位表示主机
也可以16位表示网络,16位表示主机
14为表示网络,18位表示主机

子网掩码为255.255.255.0时表示前24位为网络地址,后8位为主机地址
子网掩码为255.255.0.0时表示前16位为网络地址,后16位为主机地址

网络地址决定了这种网络中一定可以有多少个网络,譬如子网掩码为255.255.255.0时表示我们这一种网络一共最多可以有2^24个,每个这种网络中可以有2^8个主机。
如果子网掩码为255.255.0.0时,表示我们这种网络可以有2^16个网络,每个这种网络中最多可以有2^16个主机。


192.168.1.102 & 255.255.255.0 = 192.168.1.0
192.168.1.253 & 255.255.255.0 = 192.168.1.0


192.168.1.4和192.168.12.5,如果子网掩码是255.255.255.0那么不在同一网段,如果子网掩码是255.255.0.0那么就在同一个网段

什么是子网掩码?
子网掩码不能单独存在,它必须结合IP地址一起使用。子网掩码只有一个作用,就是将某个IP地址划分成网络地址和主机地址两部分。说的通俗的话,就是用来分割子网和区分那些ip是同一个网段的,那些不是同一网段的。

1.1:如下两个IP地址:
ip地址:192.168.1.1 子网掩码:255.255.255.0

ip地址:192.168.1.2 子网掩码:255.255.255.0

可以直接判断他们两是统一网段

1.2:如下两个IP地址:
ip地址:192.168.1.1 子网掩码:255.255.255.0

ip地址:192.168.1.2 子网掩码:255.255.0.0

这两个ip地址虽然在不看掩码的情况下,比较像,但他们并不是同一个网段内的。

这个可以从子网掩码来判断,

192.168.1.1 255.255.255.0是属于192.168.1.0网段的。

而192.168.1.2 255.255.0.0是属于192.168.0.0网段。

2:如何根据掩码来辨别两个IP是否属于同一网段?
2.1:如何确定子网掩码和判断ip地址的网段
通常我们在划分vlan的时候会使用以下例子:

例1:

创建vlan1:ip地址:192.168.1.1 子网掩码:255.255.255.0

创建vlan2:  ip地址: 192.168.2.1 子网掩码:255.255.255.0

可以直观的判断,他们并不是属于同一个网段,那么如何计算呢?要想判断两个ip地址是不是在同一个网段,只需将ip地址与子网掩码做与运算,如果得出的结果一样,则这两个ip地址是同一个子网当中。
 


子网掩码和网络掩码

网络掩码和子网掩码区别?_KRYON!的博客-CSDN博客_网络掩码和子网掩码的区别网络掩码和子网掩码是同一个东西?我是看这个的时候发现还有网络掩码这个概念一开始以为是同一个概念(虽然确实差不多)下面是回答:https://superuser.com/questions/315352/what-is-the-difference-between-a-subnet-mask-and-a-netmask...https://blog.csdn.net/qq_40691051/article/details/105352245


DHCP(dynamic host configuration protocl,动态主机配置协议)
(1)每台计算机都需要一个IP地址,且局域网内各电脑IP地址不能重复,否则会地址冲突。
(2)计算机的IP地址可以静态设定,也可以动态分配
(3)动态分配是局域网内的DHCP服务器来协调的,很多设备都能提供DHCP功能,譬如路由器。
(4)动态分配的优势:方便接入和断开、有限的IP地址得到充分利用



NAT(network address translation,网络地址转换协议)

内网IP转外网IP
(1)IP地址分为公网IP(internet范围内唯一的IP地址)和私网IP(内网IP),局域网内的电脑使用的都是私网IP(常用的就是192.168.1.xx)
(2)网络通信的数据包中包含有目的地址的IP地址
(3)当局域网中的主机要发送数据包给外网时,路由器要负责将数据包头中的局域网主机的内网IP替换为当前局域网的对外外网IP。这个过程就叫NAT。
(4)NAT的作用是缓解IPv4的IP地址不够用问题,但只是类似于打补丁的形式,最终的解决方案还是要靠IPv6。
(5)NAT穿透简介


内网IP和外网IP

内网ip和外网ip区别_回家养老-CSDN博客_内网ip和外网ip区别文章一:原文:https://blog.csdn.net/Alexwym/article/details/81772446我们每天都会访问各种各样的网站,比如淘宝,百度等等。不免会思考,我们的设备是如何连接上这些网址的呢?要想搞清楚这个问题,首先就得先搞清楚内网ip和外网ip的联系。如图,假设我们的计算机现在就是设备一,我们想要访问百度。如果我们正使用着校园网,那么首先我们需要先通...https://blog.csdn.net/weixin_42724467/article/details/89147214

1.公网ip具有世界范围的唯一性,而内网ip只在局域网内部具有唯一性。并且,一个局域网里所有电脑的内网IP是互不相同的,但共用一个外网IP。就像我们前面所说的你所在学校的校名在整个世界上只有一个,但是你学校里面的A栋大楼3层3号教室只有在你的校园内部才具有唯一性。别的学校也有A栋大楼3层3号教室。你只能跟快递小哥说请帮我把包裹送到xx大学,而不能说请帮我把包裹送到A栋大楼3层3号教室。

2.在局域网中,每台电脑都可以自己分配自己的IP,但是这个IP只在局域网中有效。而如果你将电脑连接到互联网,你的网络提供商的服务器会为你分配一个IP地址,这个IP地址才是你在外网的IP。两个IP同时存在,一个对内,一个对外。

3.互联网上的IP(即外网IP)地址统一由一个叫“IANA”(InternetAssigned NumbersAuthority,互联网网络号分配机构)的组织来管理。由于分配不合理以及IPv4协议本身存在的局限,现在互联网的IP地址资源越来越紧张。IANA将A、B、C类IP地址的一部分保留下来,留作局域网使用。具体如下
IP地址空间:
     a类网
     10.0.0.0~10.255.255.255
    b类网
     172.16.0.0~172.31.255.255
    c类网
     192.168.0.0~192.168.255.255

也就是说,如果你查到的ip地址在以上A、B、C类IP地址的范围内,它一定就是局域网的ip地址,否则就是公网的地址。

4.实际生活中不仅有一级NET技术,还有二级NET技术。也就是可能你的校园网关也只是个局域网。通过多级转换可以得到更多的地址。

什么是广域网(WAN、公网、外网),什么是局域网(LAN、私网、内网)?
广域网(WAN),就是我们通常所说的Internet,它是一个遍及全世界的网络。
局域网(LAN),相对于广域网(WAN)而言,主要是指在小范围内的计算机互联网络。这个“小范围”可以是一个家庭,一所学校,一家公司,或者是一个政府部门。
BT中常常提到的公网、外网,即广域网(WAN);BT中常常提到私网、内网,即局域网(LAN)。

广域网上的每一台电脑(或其他网络设备)都有一个或多个广域网IP地址(或者说公网、外网IP地址),广域网IP地址一般要到ISP处交费之后才能申请到,广域网IP地址不能重复;局域网(LAN)上的每一台电脑(或其他网络设备)都有一个或多个局域网IP地址(或者说私网、内网IP地址),局域网IP地址是局域网内部分配的,不同局域网的IP地址可以重复,不会相互影响。

广域网(WAN、公网、外网)与局域网(LAN、私网、内网)电脑交换数据要通过路由器或网关的NAT(网络地址转换)进行。一般说来,局域网(LAN、私网、内网)内电脑发起的对外连接请求,路由器或网关都不会加以阻拦,但来自广域网对局域网内电脑电脑连接的请求,路由器或网关在绝大多数情况下都会进行拦截。

通常情况下,网关或路由器对内部向外发出的信息不会进行拦截,但对来自外部想进入内部网络的信息则会进行识别、筛选,认为是安全的、有效的,才会转发给内网电脑。正是这种情况的存在,才导致了很多内网用户没有“远程”,速度也不尽如人意。


什么是网关?

什么是网关及网关作用_bestcleaner-CSDN博客_网关作用网关https://blog.csdn.net/bestcleaner/article/details/71374621


MAC地址


网络中传输的地址其实是MAC地址

MAC地址 的意义及作用_一个人的力量很渺小,一群人的力量很可怕......-CSDN博客_mac地址的作用什么是 MAC Address  MAC地址就是在媒体接入层上使用的地址,也叫物理地址、硬件地址或链路地址,由网络设备制造商生产时写在硬件内部。MAC地址与网络无关,也即无论将带有这个地址的硬件(如网卡、集线器、路由器等)接入到网络的何处,都有相同的MAC地址,它由厂商写在网卡的BIOS里。MAC地址可采用6字节(48比特)或2字节(16比特)这两种中的任意一种。但随着局域网规模越来越https://blog.csdn.net/fzf151/article/details/7573405


广播、组播和单播

广播、组播和单播是网络通信中常用的三种数据传输方式:

  1. 单播(Unicast):单播是指一对一的数据传输方式,即数据从一个发送者发送到一个接收者。在单播中,发送者和接收者之间需要建立一条独立的通信链路,数据只能被一个接收者接收。

  2. 广播(Broadcast):广播是指一对多的数据传输方式,即数据从一个发送者发送到所有的接收者。在广播中,发送者不需要知道接收者的地址,数据会被传输到所有的网络节点中,所有节点都会接收到这份数据。
    值得注意的是:使用广播传输数据 指定的数据接收方的IP地址 必须是xxx.xxx.xxx.255
                               指定的数据接收方MAC地址必须是全为1 也就是ff:ff: ff: ff:ff:ff
    广播传输的数据可以通过网络节点中的所有主机的数据链路层和网络层(原因在于指定的IP地址和MAC地址);但是无法通过所有的传输层 这是因为进行数据传输需要指定MAC地址、IP地址、端口号。所以虽然通过广播可以实现让当前网络节点中的所有主机在数据链路层和网络层接收到数据,但是无法让所有的主机都在传输层接收到数据,只有主机中存在一个端口,和数据发送方在传输时指定的端口号相同 才能在传输层接收到数据。
    举个例子:主机1 mac地址:xx:xx:xx:xx:xx:xx IP地址:191.168.1.xxx 端口号2
                      主机2 mac地址:xx:xx:xx:xx:xx:xx IP地址:191.168.1.xxx 端口号3
                      主机3 mac地址:xx:xx:xx:xx:xx:xx IP地址:191.168.1.xxx 端口号200
    此时数据发送方指定的 mac地址:ff:ff: ff: ff:ff:ff IP地址:191.168.1.255 端口号200
    就代表进行了广播 数据到达了主机1、2、3的数据链路层和网络层,但是由于指定的端口号为200,所以只有主机3在传输层接收到了数据

  3. 组播(Multicast):组播是指一对多的数据传输方式,但是相比于广播,组播只会将数据传输到指定的一组接收者。在组播中,发送者需要知道接收者的组地址,数据只会被传输到这个组中的所有接收者。

总之,单播、广播和组播是网络通信中常用的三种数据传输方式,每种方式都有其优缺点,根据不同的需求选择不同的方式可以提高网络通信的效率和可靠性。

 传输层协议中只有UDP可以广播和组播


字节序

字节序指的是计算机中多字节整型数据的存储格式,通常有大端序和小端序两种。

//大端序与小端序的名称来源于《格利佛游记》,主人公在小人国时发现该国因为水煮蛋该从大端(Big-End)剥开还是小端(Little-End)剥开分为两派而引发激烈的战争。1980年,Danny Cohen在自己的论文中第一次引用了该两个词。

    大端序(Big-Endian):高地址存储数据低位,低地址存储数据高位

    小端序(Little-Endian):高地址存储数据高位,低地址存储数据低位

通常情况下,网络通信中使用大端序传输数据,而操作系统内通常使用小端序存储数据。

由于在网络通信中,发送方和接收方可能使用不同的字节序,因此为了保证数据的正确性,二者在通信前必须统一字节序。


组网方式

1.网络互联:使用集线器将少量主机连在一起

在这里插入图片描述

2.局域网(LAN):使用交换机和路由器将主机连接,可以自由组合三种方式

组网方式:

        <1>.交换机

        <2>.路由器

        <3>.交换机+路由器

在这里插入图片描述

3.广域网(WAN):广域网和局域网知识相对的概念

例如:一个学校之间的网络就可以成为局域网,而一个国家,多个国家之间可以称为广域网,覆盖的区域不同在这里插入图片描述

组网方式:公网上,网络结点组成,每一个结点可以是:

在这里插入图片描述

二、计算机网络的拓扑结构

    网络拓扑结构(Topology),指的是网络中各个结点的互连方式,也就是网络链路与结点的几何布局,它描述了各个结点间的物理与逻辑位置。

常见的基本网络拓扑结构有5种:

    网状(Mesh)

    星状(Star)

    树状(Tree)

    总线型(Bus)

    环状(Ring)

1、网状拓扑

    在网状拓扑内,每一个设备与其他的设备都会有一条专线连接。

相较于其他拓扑结构,网状拓扑有以下优点:

    -设备之间数据负载由专线承担,避免共享链路中网络拥堵问题

    -良好的健壮性。当其中某个线路不可用时,整个网络不会瘫痪

    -隐私性和安全性高。每个消息都是专线传输,只有唯一的发送方与接收方

    -故障检测与故障隔离十分容易。

当然,网状拓扑也有其缺点:

    -需要的网络连接电缆数量过大。具有N个设备的网状结构需要N*(N-1)/2条物理通道,每个设备需要N-1个输入/输出端口

2、星状拓扑

    在星状拓扑中,每个设备与中央控制器相连。如果一个设备要向另一个设备发送数据,则需要先将数据发送到中央控制器,再由中央控制器将数据转发给对应设备。

相较于其他拓扑结构,星状拓扑有以下优点:

    -拓扑结构简单。每个设备只需一条电缆和一个输入/输出端口即可与其他设备建立连接

    -具有良好的健壮性。如果一条链路失效,只有连接该链路的设备受影响,其余设备不会被影响

    -便于管理,故障检测与故障隔离很容易。

当然,星状拓扑也有其缺点:

    -中央控制器是整个网络的瓶颈。如果中央控制器出现故障则整个网络会瘫痪。同时,中央控制器吞吐数据的速率也直接影响整个网络的速率。

3、树状拓扑

    树状拓扑结构可以看做是星状拓扑结构的扩展。在树状拓扑结构中,绝大多数的设备首先连接在次级控制器上,再由次级控制器连接到中央控制器上。

树状拓扑结构的优点和缺点基本与星状拓扑结构类似。但次级控制器的引入带来了另外的优点:

    -允许更多设备相连并且增加了信号在设备间的传输距离

    -允许网络隔离不同计算机的通信,以及为不同的计算机配置通信优先级

4、总线型拓扑

    总线型拓扑是由一条长电缆组成的主干和连接在上面的网络设备组成。网络节点通过引出线和分接头连接到总线电缆上。

总线型拓扑有以下优点:

    -信息传输不存在路由与转发的问题

    -易安装,主干电缆可以铺设在最有效的路径上,然后将网络结点通过各种分支引出线连接到主干上

总线型拓扑具有以下缺点:

    -故障隔离和重新配置困难,加入或移除设备需要改动或者更新主干,总线的故障会终止所有传输

    -由于信号衰减,总线的长度和连接的设备数会受到限制

5、环状拓扑

    环状拓扑结构是网络中各个设备通过一条首尾相连的通信链路连接的一个闭合环形结构。每个设备只与其两侧的两个设备之间有专用链路连接。信号在环中从一个设备到另一个设备单向传输,直至到目的地为止。

 


OSI七层模型

传输层关注的是向位于不同主机(或有时候位于同一主机)上的应用程序提供端对端的通信服务

网络层关注的是如何将包(数据)从源主机发送到目标主机。

数据链路层关注的是在一个网络的物理链接上传输数据

太厉害了,终于有人能把TCP/IP 协议讲的明明白白了

  计算机网络由若干个相互连接的结点构成,这些结点之间要不断进行数据交换。要进行正确的数据传输,每个结点就必须遵循一些实现约定好的规则,这些规则就是网络协议。

网络协议是在主机与主机之间、主机与通信子网之间或通信子网中各通信结点之间通信时使用的、通信双方必须遵守的、实现约定好的规则、标准或约定。一个合格的网络协议主要有以下三个要素:

  •     -语法要求:即数据与控制信息的结构或格式。
  •     -语义要求:即需要发出何种控制信息、完成何种动作、做出何种响应等,同时需要规定差        错处理。
  •     -时序(同步)要求:即事件实现顺序的详细说明,包括速度匹配和顺序等。

当然,对于复杂的网络协议,其结构最好是层次式的。分层的协议有以下好处:

  •     -各结构间独立,一个层次无需知道它下面的层次如何实现,而仅仅通过层间提供的接口实现服务。
  •     -灵活性好。当某一层次发生变化,只要接口关系保持不变,其他层不会受到影响。
  •     -结构上可分隔开,各层可以采用最合适的技术实现。
  •     -易于实现和维护。分层结构使一个复杂的系统的实现和调试变得简单。
  •     -有利于标准化作业。各个层次的功能以及向其他层提供的服务都有精确说明。
     

在OSI参考模型中,有7个层次的体系结构

//需要注意的是,OSI仅是一个理想化的模型,尚未有完整的实现,但模型本身的指导意义非常通用

 1.物理层

    物理层规定在一个结点内如何把计算机连接到介质上。规定了机械的、电气的功能。该层负责建立、保持和拆除物理链路;规定如何在此链路上传送比特流;比特如何编码,使用的电平极性,连接插头插座的插脚如何配置等。物理层传送数据单位是比特(bit)。

 2.数据链路层

    数据链路层把相邻两个结点间不可靠的物理链路变成可靠无差错的逻辑链路。包括把原始比特流进行分帧、排序、设置检错、确认、重发、流量控制等功能。数据链路层传送数据单位是帧(frame),每帧包括一定数量的数据和一些必要的控制信息,在每帧的控制信息中,包括同步信息、地址信息、差错控制信息、流量控制信息等;同物理层相似,数据链路层负责建立、维护和释放数据链路。
 

 3.网络层

    网络层连接网络中任何两个计算机结点,从一个结点上接收数据,并正确地传送到另一个结点。在网络层,传送数据单位是分组或包( packet)。网络层的主要任务是要选择合适的路由和交换结点,透明地向目的站交付发送站所发的分组或包,这里的“透明”表示收发两端好像是直接连通的。另外,网络层还要解决网络互连、拥塞控制和记账等问题。
 

上述3层组成了所谓的通信子网,用户计算机连接到此子网上。通信子网负责把一个计算机上的数据可靠地传送到另一台计算机,但并未实现两台主机上的进程之间的通信。通信子网的主要功能是面向通信的。

 4.传输层

    传输层真正地实现了“端到端”通信,把数据可靠地从一方的用户进程或程序送到另一方的用户进程或程序。这一层的控制通常由通信两端的计算机完成,中间结点一般不提供这一层的服务,这一层的通信与通信子网无关。从这一层开始的以上各层全部是针对通信的最终的“源端-目的端”计算机进程的。传输层传送数据单位是报文(message)。

    传输层向上一层提供一个可靠的端到端的服务,使上一层看不见下面几层的通信细节。正因为如此,传输层成为网络体系结构中最关键的一层。传输层的功能主要在主机内实现。而物理层、数据链路层及网络层的功能均在报文接口机中实现。传输层以上各层的功能通常在主机中实现。
 

   5.会话层

    会话层允许两个计算机上的用户进程建立会话连接,双方相互确认身份,协商会话连接的细节。它可管理会话是双向同时进行的,还是任何时刻只能一个方向进行。在后一种情况下,会话层控制哪一方有权发送数据。会话层还提供同步点机制。在数据流中插入同步点机制,在每次网络出现故障后可以仅仅重传最近一个同步点以后的数据,而不必从头开始。

    传输层和会话层为两台计算机上的用户进程或程序之间提供了正确传送数据的手段。

    6.表示层

    表示层主要解决用户信息的语法表示问题。表示层将数据从适合于某一系统的语法转变为适合于OSI系统内部使用的语法。具体地讲,表示层对传送的用户数据进行翻译或解释、编码和变换,使不同类型的计算机对数据信息的不同表示方法可以相互理解。另外,数据加密、解密、信息压缩等都是本层的典型功能。

    7.应用层

    应用层确定进程之间通信的性质,以满足用户的需要。它负责用户信息的语义表示,并在两个通信者之间进行语义匹配。具体地说,应用层处理用户的数据和信息,由用户程序(应用程序)组成,完成用户所希望的实际任务。这一层包括最终用户普遍需要的协议,如虚拟终端协议、文件传送协议和电子邮件等。

总结:

1.对协议的简单理解:本质上是数据格式的定义。而知名的数据格式,大家普遍遵循的规定,就属于协议

2.OSI七层模型:一种网络分层的设计方法论,比较复杂且不实用,落地几乎都是TCP/IP四层,五层模型

在这里插入图片描述


 TCP/IP五层(四层)模型

1、TCP/IP网络体系结构简介

    OSI参考模型的制定具有十分重大的意义,但是由于TCP/IP协议使用时间较长,再加上简洁、实用而成为了实际上的工业标准。在实际运行的系统中采用OSI参考模型的并不多。

    TCP/IP(Transmission Control Protocol/Internet Protocol, 传输控制协议/网际协议)是世界上最大的计算机网络Internet的运行基础,是目前为止应用最广泛的网络通信协议,现在已称为企业网络的事实标准。大多数网络操作系统都以TCP/IP协议作为缺省网络协议。

    TCP/IP协议由5层构成:物理层、数据链路层、网络层、传输层和应用层,其中物理层和数据链路层有时也被合并称为“网络接口层”即称为TCP/IP四层协议。其中TCP/IP协议中的应用层大致等同于OSI参考模型的会话层、表示层和应用层的结合。

太厉害了,终于有人能把TCP/IP 协议讲的明明白白了

2、TCP/IP网络体系结构成员协议简介

TCP/IP是一个协议栈,包含了众多成员协议,从而构成一个整体。每一层的成员协议如下:

    1).TCP/IP的物理层和数据链路层(网络接口层)

    TCP/IP协议并未提供物理层和数据链路层的传输协议,其物理层和数据链路层的传输协议由底层提供,TCP/IP协议仅为其提供接口,并与其独立。正因如此,物理层与数据链路层有时也被合称为“网络接口层”。

  

    2).TCP/IP的网络层

    TCP/IP协议的网络层主要功能是寻址、数据打包和路由选择。网络层的主要协议有:

    ARP        地址解析协议。将IP地址解析为结点的物理地址,以便于物理设备(网卡)按该地址接收数据

    RARP      反向地址解析协议。将物理地址解析成为IP地址

    ICMP      Internet控制消息协议,用于传送差错报文及其他控制信息。ICMP向其他结点传输的信息类型有:

            -通知目的结点不可到达

            -发送特定路由或路由器的差错状态信息

            -对可达结点状态的请求或响应信息

            -超时(生存期终止)数据报通知

    IGMP    Internet组管理协议,负责对IP组播进行管理,例如组播组的建立和删除、组成员的建立和删除等

    3).TCP/IP的传输层

    TCP/IP协议栈提供了两个传输层协议:

    TCP         传输控制协议。可靠的面向连接的传输层协议。

    UDP        用户数据报协议。不可靠的面向无连接的传输层协议。

    4).TCP/IP的应用层

    应用层为应用程序提供访问低层服务的能力,并定义应用程序用于交换数据的协议。应用层的协议有许多,常用的协议有:

    FTP         文件传输协议。用于交互式的文件传输。

    Telnet     虚拟终端协议。用于登录远程主机。

    NNTP     网络新闻传输协议。用于传送网络新闻消息。

    SMTP     简单邮件传输协议。用于邮件服务器之间的邮件传送。

    HTTP      超文本传输协议。用于传输Web页面文件。

    POP        邮局协议。用于从邮件服务器上取回邮件。

    DNS        域名解析服务。用于将域名解析为IP地址。

    SNMP     简单网络管理协议。用于在网络管理控制台和网络设备(路由器、网桥、集线器等)之间选择和交换网络管理信息。

既:

五层模型:除去OSI七层模型的表示层和会话层

四层模型:除去OSI七层模型的表示层,会话层和物理层

在这里插入图片描述

 注意:

应用程序实现对应用层的封装分用

对于一台主机, 它的操作系统内核实现了从传输层到物理层的内容(四层封装分用);对于一台路由器, 它实现了从网络层到物理层(下三层封装分用);对于一台交换机, 它实现了从数据链路层到物理层(下两层分装分用);对于集线器, 它只实现了物理层;
 

3. TCP/IP 的具体含义

从字面意义上讲,有人可能会认为 TCP/IP 是指 TCP 和 IP 两种协议。实际生活当中有时也确实就是指这两种协议。然而在很多情况下,它只是利用 IP 进行通信时所必须用到的协议群的统称。具体来说,IP 或 ICMP、TCP 或 UDP、TELNET 或 FTP、以及 HTTP 等都属于 TCP/IP 协议。他们与 TCP 或 IP 的关系紧密,是互联网必不可少的组成部分。TCP/IP 一词泛指这些协议,因此,有时也称 TCP/IP 为网际协议群。

互联网进行通信时,需要相应的网络协议,TCP/IP 原本就是为使用互联网而开发制定的协议族。因此,互联网的协议就是 TCP/IP,TCP/IP 就是互联网的协议。

太厉害了,终于有人能把TCP/IP 协议讲的明明白白了

4. 数据包

包、帧、数据包、段、消息

以上五个术语都用来表述数据的单位,大致区分如下:

  • 包可以说是全能性术语;
  • 帧用于表示数据链路层中包的单位;
  • 数据包是 IP 和 UDP 等网络层以上的分层中包的单位;
  • 段则表示 TCP 数据流中的信息;
  • 消息是指应用协议中数据的单位。

每个分层中,都会对所发送的数据附加一个首部,在这个首部中包含了该层必要的信息,如发送的目标地址以及协议相关信息。通常,为协议提供的信息为包首部,所要发送的内容为数据。在下一层的角度看,从上一层收到的包全部都被认为是本层的数据。

太厉害了,终于有人能把TCP/IP 协议讲的明明白白了

数据包首部

网络中传输的数据包由两部分组成:一部分是协议所要用到的首部,另一部分是上一层传过来的数据。首部的结构由协议的具体规范详细定义。在数据包的首部,明确标明了协议应该如何读取数据。反过来说,看到首部,也就能够了解该协议必要的信息以及所要处理的数据。包首部就像协议的脸。

对封装分用的理解

就好像快递包装和拆包装,快递要包装后才可以运输,要拆包装才可以使用。通过网络传输数据也是这样。

1.封装:发送数据时,从高到低的顺序,按照对应的网络分层协议对数据进行包装
在这里插入图片描述

例如:

在这里插入图片描述

2.分用:封装的逆过程:接收数据时,从低到高的顺序,按照对应的网络分层协议,解析数据

在这里插入图片描述

例如:

在这里插入图片描述

5. 数据处理流程

下图以用户 a 向用户 b 发送邮件为例子:

太厉害了,终于有人能把TCP/IP 协议讲的明明白白了

数据处理流程如下:

① 应用程序处理

首先应用程序会进行编码处理,这些编码相当于 OSI 的表示层功能;

编码转化后,邮件不一定马上被发送出去,这种何时建立通信连接何时发送数据的管理功能,相当于 OSI 的会话层功能。

② TCP 模块的处理

TCP 根据应用的指示,负责建立连接、发送数据以及断开连接。TCP 提供将应用层发来的数据顺利发送至对端的可靠传输。为了实现这一功能,需要在应用层数据的前端附加一个 TCP 首部。

③ IP 模块的处理

IP 将 TCP 传过来的 TCP 首部和 TCP 数据合起来当做自己的数据,并在 TCP 首部的前端加上自己的 IP 首部。IP 包生成后,参考路由控制表决定接受此 IP 包的路由或主机。

④ 网络接口(以太网驱动)的处理

从 IP 传过来的 IP 包对于以太网来说就是数据。给这些数据附加上以太网首部并进行发送处理,生成的以太网数据包将通过物理层传输给接收端。

⑤ 网络接口(以太网驱动)的处理

主机收到以太网包后,首先从以太网包首部找到 MAC 地址判断是否为发送给自己的包,若不是则丢弃数据。

如果是发送给自己的包,则从以太网包首部中的类型确定数据类型,再传给相应的模块,如 IP、ARP 等。这里的例子则是 IP 。

⑥ IP 模块的处理

IP 模块接收到 数据后也做类似的处理。从包首部中判断此 IP 地址是否与自己的 IP 地址匹配,如果匹配则根据首部的协议类型将数据发送给对应的模块,如 TCP、UDP。这里的例子则是 TCP。

另外吗,对于有路由器的情况,接收端地址往往不是自己的地址,此时,需要借助路由控制表,在调查应该送往的主机或路由器之后再进行转发数据。

⑦ TCP 模块的处理

在 TCP 模块中,首先会计算一下校验和,判断数据是否被破坏。然后检查是否在按照序号接收数据。最后检查端口号,确定具体的应用程序。数据被完整地接收以后,会传给由端口号识别的应用程序。

⑧ 应用程序的处理

接收端应用程序会直接接收发送端发送的数据。通过解析数据,展示相应的内容。


网络数据传输

<1>局域网

ARP协议:IP转MAC
RARP协议:MAC转IP





(1)认识IP和MAC

IP(对应网络模型中的网络层 用来识别 TCP/IP 网络中互连的主机和路由器):

网络世界的门牌号:IP地址

IP的格式:xxx.xxx.xxx.xxx
IP由四个部分组成,每个部分都是0-255.
网络号:前三个部分组成(用来标识网段),前三个部分相同,标识在一个网段(因为两个IP地址和各自的子网掩码相与的结果相同 就是属于同一网段 而子网掩码一般都是xxx.xxx.xxx.0 所以一般只要IP地址前三部分相同 就是在同一网段)
主机号:最后一个部分用来标识主机号
IP分为A-E五大类,部分范围是局域网IP,部分是广域网IP,可以根据规范,知道某个IP是局域网IP还是公网IP
注意: 局域网内(局域网IP):网段唯一,同一个网段,主机号唯一
公网(公网IP):公网IP是唯一的

MAC(对应网络模型中的数据链路层 用来识别同一链路中不同的计算机):

和网卡硬件绑定的,全球唯一
作用:网络数据传输定位网卡硬件的位置,一个主机可能有多个网卡(例如蓝牙连接,无线连接,有线连接的网卡),电脑硬件定位数据发送的目的位置只能使用MAC

总结:
IP地址描述的是路途总体的起点和终点。(给人用的,网络主机的逻辑地址)
MAC地址描述的是路途上的每一个区间的起点和终点(给电脑硬件用的,网络主机的物理地址)

IP地址和MAC地址的区别:

  • IP地址是服务商给你的,mac地址是你的网卡物理地址;
  • IP地址局域网内可以随便更改,但是mac地址一般不能更改;
  • 长度不同。IP地址为32位,MAC地址为48位;
  • 寻址协议层不同。IP地址应用于OSI第三层,即网络层,而MAC地址应用在OSI第二层,即数据链路层。

IP地址和MAC地址的关系:

  • 首先,IP 间的通信依赖 MAC 地址。
  • 使用 ARP 协议将IP地址转换为 MAC 地址进行通信:
  • 在网络上,通信的双方在同一局域网(LAN)内的情况是很少的,通常是经过多台计算机和网络设备中转才能连接到对方。而在进行中转时,会利用下一站中转设备的 MAC 地址来搜索下一个中转目标。这时,会采用 ARP 协议(Address Resolution Protocol)。

ARP 是一种用以解析地址的协议,根据通信方的 IP 地址就可以反查出对应的 MAC 地址。

图文形象说明:

ip和mac协同通信

流程解说:

发送端(名称为A,IP地址为IP_A,MAC地址为MAC_A)向接收端(名称B,IP地址为IP_B,MAC地址为MAC_B)发送数据。这两台主机之间不可能是直接连接起来的,因而数据包在传递时必然要经过许多中间节点(如路由器,服务器等等),我们假定在传输过程中要经过C1、C2其MAC地址分别为M1,M2)两个节点。

A在将数据包发出之前,先发送一个ARP请求,找到其要到达IP_B所必须经历的第一个中间节点C1的MAC地址M1,然后在其数据包中封装这些地址:IP_A、IP_B,MAC_A和M1。当PAC传到C1后,再由ARP根据其目的IP地址IP_B,找到其要经历的第二个中间节点C2的MAC地址M2,然后再将带有M2的数据包传送到C2。如此类推,直到最后找到带有IP地址为IP_B的B主机的地址MAC_B,最终传送给主机B。

在传输过程中,IP_A、IP_B和MAC_A不变,而中间节点的MAC地址通过ARP在不断改变(M1,M2),直至目的地址MAC_B。 
 

(2)网络数据传输的特性

1.IP,MAC起的作用
2.封装分用——发送数据从高到低封装,接收数据从低到高分用
3.结合IP,MAC,理解网络数据传输,本质上是一跳一跳的传输数据

在这里插入图片描述

首先根据目的主机发送http请求,从源IP发送数据到目的IP
从源MAC(1)发送数据到目的MAC(2),然后MAC(2)对数据进行封装和分用,再以MAC(2)为源MAC,目的MAC为MAC(3),以此,发送数据到最终目的MAC。
注意: 接收数据报的主机:可能在一些情况下(广播或者转发),出现目的MAC不是我,我也能收到的情况(后面会提到)。

五元组:
源IP,目的IP,源端口,目的端口,协议号

IP:标识主机,给人用
源IP:发送数据的主机
目的IP:接收数据的主机

端口号:
源端口:标识发送数据的进程
目的端口,标识接收数据的进程
协议号:进程需要封装,解析数据报的数据格式

DNS协议:
作用:域名转IP
在这里插入图片描述

主机/路由器:都存在DNS缓存
域名查询的方式:上图树形结构从下往上查找(缓存,域名服务器)。
先在主机/路由器的DNS缓存中找,如果找不到,依次向上

特殊的IP,域名:本机IP为127.0.0.1,本机域名为localhost

(3)网络数据传输流程


ARP/RARP协议:

ARP协议:IP转MAC
RARP协议:MAC转IP

注意:交换机和集线器自己是没有MAC地址的,都是通过转发(不会修改源MAC和目的MAC)
交换机有MAC地址转换表,可以根据MAC找到对应的端口,而集线器没有这个功能

1)网络互联的方式

在这里插入图片描述
首先介绍集线器:如上图,网络数据传输时,直接转发到其他所有端口(工作在物理层

网络数据传输的过程:

1)ARP缓存表找到了

在这里插入图片描述

1.主机1发送数据到主机3(http://主机3:80)
2.主机1查找本机的ARP缓存表,根据ARP协议,找到目的MAC
3.数据报由主机1,发送到集线器(数据报中的源MAC(主机1),目的MAC(主机3)真实的数据报
4.集线器转发数据报到除主机1的其他所有相连的主机(主机2,主机3)
5.主机2接收:数据报中,目的MAC不是我,丢弃
主机3接收,数据报中,目的MAC是我,接收
目的IP是我,交给对应端口处理,如果不是我,执行上述网络传输(一跳一跳的过程)

2)ARP缓存表没找到

在这里插入图片描述

 在这里插入图片描述

1.主机1发送数据到主机3,http://主机3:80
2.主机1查找本机的ARP缓存表,发现找不到
3.主机1发送广播数据报(非真实数据,只是要求对应主机返回MAC:我要IP为主机3的MAC,谁是主机3,快告诉我)

4.集线器转发到主机2,主机3
5.主机2接收:要求的IP不是我,丢弃
主机3接收:要求的IP是我,返回我的MAC
6.主机1收到主机3的返回数据(IP,MAC)更新自己的ARP缓存表
7.主机1发送真实的数据到主机3

注意:使用集线器的缺陷
网络冲突,这样构成的网络区域叫冲突域/碰撞域(例如,房间里有多个人说话,那么其中某一个人说话就听不清楚了)
 

2).局域网交换机组网的方式

在这里插入图片描述

首先介绍交换机,交换机的作用:
MAC地址转换表:保存连接的主机MAC和端口的映射,目的MAC是谁,直接转发到对应的端口(不像集线器,发送到所有端口),不会产生冲突域。

在这里插入图片描述

1.主机1发送数据到主机3 ,http://主机3:80
2.主机1查找本机的ARP缓存表,如果找到,主机1发送数据到主机3。如果找不到,发送广播数据报,让IP为主机3的告诉我,你的MAC
3.交换机转发到其他所有端口(广播)
4.主机2丢弃,主机3返回自己的MAC
5.交换机知道主机3的MAC,主机1知道主机3的MAC(更新ARP缓存表)
注意:上述五个步骤,都是根据IP找MAC,和集线器的流程相似,下面的步骤时根据MAC找端口
6.主机1发送真实数据给交换机
7.交换机查找自己的MAC地址转换表,通过MAC找端口,发送数据到对应的端口
8.主机3接收,目的MAC是我,目的IP也是我
这种网络数据传输的方式就像:先问张三的手机号,再打电话给张三,对别人没有影响
 

3)局域网交换机+路由器组网的方式
注意:单独由路由器组网的方式,和上述由交换机单独组网的方式相同
首先介绍路由器,这里介绍两种:
<1>LAN口连接局域网,为主机分配局域网IP,分配的局域网IP都是一个网段(路由器下连接多个主机的类型)
路由器还有个网卡:绑定局域网的IP,和下面连接的主机进行信息交互用的


在这里插入图片描述

<2>LAN口是网卡。每个LAN口都可以连接类似交换机组网的方式

在这里插入图片描述

主机上的网络信息:

在这里插入图片描述

第二种路由器组网方式:

在这里插入图片描述

1.主机1发送数据到主机2:http://192.168.2.y:8080/xxx
2.通过目的IP+子网掩码,计算出目的主机和本机是否在一个网段
3.如果是,不需要使用路由器,和上述使用交换机组网方式一样
4.如果不是,表示我主机1和交换机处理不了,要发送给网关转发(网关就类似于IP的管理者,能查询其他主机的IP)
5.数据报发送给网关设备

6.路由器接收到数据报,分用:物理层到网络层,网络层分用,所有可以获取到目的IP
7.路由器查找自己的ARP缓存表(IP找MAC)
8.找不到,路由器发广播,主机2在哪,告诉我你的MAC
9.有了MAC,直接发到主机3

在这里插入图片描述

 目的MC:通过路由器网关的IP在主机1的ARP缓存表中,获取网关的MAC

 

<2>广域网传输流程

1.NAT和NAPT

NAT协议:局域网IP映射公网IP(也就是将内网IP转换为外网IP)
NAPT协议:局域网IP+局域网端口映射----->公网IP+公网端口

2.传输流程

在这里插入图片描述

 结合上图,理解广域网传输流程
主机1发送http://www.baidu.com网络流程

传输流程
1首先主机1发送http请求,使用DNS协议:进行域名转IP
域名转IP:首先在本机DNS缓存表找,如果找不到---->向上查找------>如果根域名服务器也找不到,表示公网上没有该域名的主机

2. 找到IP,数据报IP部分,PORT部分都有了:

在这里插入图片描述

根据目的IP计算是否和主机在同一个网段
主机1的IP+子网掩码 计算出------>主机1的网段
目的IP+子网掩码 计算出------->目的主机的网段
通过上述计算,判断目的IP和主机是否在同一个网段

如果是同一个网段,和局域网传输一样
如果不是同一个网段:发送数据到网关(可以搜搜什么是网关)
找网关的MAC:
在这里插入图片描述

找到网关的MAC之后,将http数据重新封装,交由交换机转发
交换机转发:在MAC地址转换表(MAC映射端口),通过目的MAC找端口(交换机的屁股口)
注意:这个过程没有封装和分用

注意:前五个步骤,和路由器组成的局域网传输流程一样 参考:局域网传输

路由器接收,分用数据报
注意:路由器会根据最短路径算法,计算出下一个发送数据的设备,会离目的IP更近一步

在这里插入图片描述

. 上述步骤之后,数据报由局域网到广域网进行传输
路途中的设备:

在这里插入图片描述

8. 数据报到达百度服务器之后
在这里插入图片描述

**9.**数据由百度服务器返回,路途上经过的设备传输流程和步骤七相同(但是不一定是原路返回)
**10.**路由器1接收响应数据(对接收的数据进行分用,修改,封装)

在这里插入图片描述11. 之后的步骤,和局域网传输相同
主机接收数据报,分用

<3>端口号(传输层)

在谈论端口号之前我们必须先明白了解传输层的作用:

在这里插入图片描述

传输层:为相互通信的应用程序提供逻辑通信

我们都知道,在IP层协议能够把源主机A发出的分组,按照源IP地址,送到目的IP地址,那么,传输层是做什么的呢?

从网络层来说,通信的是两个主机(两个局域网)IP数据报的首部明确标志了这两台主机的IP地址,但这是两台主机的沟通远远不够,因为真正需要通信的是两台主机上的进程。IP协议仅仅能够把数据传到目的主机,但这远远不够,这个分组仅仅停留在了主机的网络层而没有交付到主机的应用层。
从传输层来看,通信的真正端点并不是主机而是“主机的进程”
所以,传输层和网络层的明显区别是:网络层为主机之间提供逻辑通信,而运输层提供端到端的逻辑通信

也就是说端口号可以帮助我们找到需要网络通信的进程

什么是端口号?(重点看看)


我们之前在初识进程中知道,单个计算机进程是用进程标示符(PID)标志的。但是在互联网的大环境下,操作系统很多,不同的操作系统有不同的进程标识符,所以仅仅用进程标示符是不足够的。
因此,为了让不同操作系统的计算机应用程序能够互相通信,就必须用统一的方法对进程进行标志
但就算使用统一的标示符进行标识,也存在问题

1.进程的创建和撤销是动态的,通信的一方几乎无法识别对方的进程
2.我们需要主机提供的功能来识别通信的重点,但是我们无法识别具体的进程是哪个
所以:运输层使用“”协议端口号“来解决这个问题,就是端口号。
端口号解决了传输层的分用问题
 

可以这么形容:

如果 IP 是用来定位街区的,那么端口就是对应于该街区中每一户的门牌号。在通讯过程中,数据通过各种通讯协议最终抵达设备(如计算机)后,这里的设备就相当于一个街区,而在设备计算机内部有很多程序在跑,数据进来之后,必须要给它一个对应的门牌号(即端口号),程序才方便进行后续操作。

端口号属于传输协议的一部分,因此我们可以说,数据通过 IP 地址发送对应的数据到指定设备上,而通过端口号把数据发送到指定的服务或程序上。

程序一般不止是监听指定的端口号,而且也会明确对应的传输协议。所以我们在进行数据传输的时候,既要指定对应的端口号,也要指定对应的通讯协议,很多人仅仅会说:程序 A 监听着 33001 端口,这个是不正确的,至少是不完全正确的。相应的,我们应该这样说:程序 A 使用 TCP 协议,监听 33001 端口,当然你也可以说:程序 A 使用 UDP 协议,监听 33001 端口。

指定传输协议和端口,显而易见的好处在于,当我们进行端口转发或者构建网络防火墙的时候,我们可以很方便的通过协议和端口进行隔离。以防止不可预见的意外发生。对于计算机来说,通过这种方式可以防止外网各种不必要的数据,进入本地局域网。

你可能会想,这么多端口号,如果大家都用同一个,那不是也有冲突。没错,这就需要一个专门的组织来管理它们,IANA( Internet Assigned Numbers Authority 即互联网号码分配局 ),它负责管理端口注册。大多数主流的程序,都有一个明确的已注册端口,比如常见的 FTP 监听 20、 21 端口,而 HTTP 服务监听 80 端口等。如果有一个程序想注册某个端口,那么 IANA 会先去查一查这个端口是否已被注册,如果已经被注册了,它则会拒绝申请。

端口号根据范围分为三种:

1 . Well-Known Ports(即公认端口号)

它是一些众人皆知著名的端口号,这些端口号固定分配给一些服务,我们上面提到的 HTTP 服务、 FTP服务等都属于这一类。知名端口号的范围是:0-1023。

2 . Registered Ports(即注册端口)

它是不可以动态调整的端口段,这些端口没有明确定义服务哪些特定的对象。不同的程序可以根据自己的需要自己定义,注册端口号的范围是:1024-49151。

3 . Dynamic, private or ephemeral ports(即动态、私有或临时端口号)

顾名思义,这些端口号是不可以注册的,这一段的端口被用作一些私人的或者定制化的服务,当然也可以用来做动态端口服务,这一段的范围是:49152–65535。
 

 根据端口号识别应用

一台计算机上同时可以运行多个程序。传输层协议正是利用这些端口号识别本机中正在进行通信的应用程序,并准确地将数据传输。

太厉害了,终于有人能把TCP/IP 协议讲的明明白白了

通过端口号识别应用

 通过 IP 地址、端口号、协议号进行通信识别

  • 仅凭目标端口号识别某一个通信是远远不够的。

太厉害了,终于有人能把TCP/IP 协议讲的明明白白了

太厉害了,终于有人能把TCP/IP 协议讲的明明白白了

通过端口号、IP地址、协议号进行通信识别

  • ① 和② 的通信是在两台计算机上进行的。它们的目标端口号相同,都是80。这里可以根据源端口号加以区分。
  • ③ 和 ① 的目标端口号和源端口号完全相同,但它们各自的源 IP 地址不同。
  • 此外,当 IP 地址和端口号全都一样时,我们还可以通过协议号来区分(TCP 和 UDP)。

端口号的确定

  • 标准既定的端口号:这种方法也叫静态方法。它是指每个应用程序都有其指定的端口号。但并不是说可以随意使用任何一个端口号。例如 HTTP、FTP、TELNET 等广为使用的应用协议中所使用的端口号就是固定的。这些端口号被称为知名端口号,分布在 0~1023 之间;除知名端口号之外,还有一些端口号被正式注册,它们分布在 1024~49151 之间,不过这些端口号可用于任何通信用途。
  • 时序分配法:服务器有必要确定监听端口号,但是接受服务的客户端没必要确定端口号。在这种方法下,客户端应用程序完全可以不用自己设置端口号,而全权交给操作系统进行分配。动态分配的端口号范围在 49152~65535 之间。

端口号与协议

  • 端口号由其使用的传输层协议决定。因此,不同的传输层协议可以使用相同的端口号。
  • 此外,那些知名端口号与传输层协议并无关系。只要端口一致都将分配同一种应用程序进行处理。


传输层协议

 TCP协议

TCP是面向连接的字节流传输层服务,使用流式套接字,在内核有发送和接收缓冲区

关于TCP理解的重点
(1)TCP协议工作在传输层,对上服务socket接口,对下调用IP层
(2)TCP协议面向连接(就好像打电话一样只有建立连接才可以进行通信),通信前必须先3次握手建立连接关系后才能开始通信。
(3)TCP协议提供可靠传输,不怕丢包、乱序等。

TCP如何保证可靠传输
(1)TCP在传输有效信息前要求通信双方必须先握手,建立连接才能通信
(2)TCP的接收方收到数据包后会ack给发送方,若发送方未收到ack会丢包重传(丢包重传机制)
(3)TCP的有效数据内容会附带校验,以防止内容在传递过程中损坏
(4)TCP会根据网络带宽来自动调节适配速率(滑动窗口技术)
(5)发送方会给各分割报文编号,接收方会校验编号,一旦顺序错误即会重传。

    TCP提供了一种可靠的面向连接的字节流传输层服务面向连接是指两个使用TCP的应用在彼此交换数据之前必须先建立一个TCP连接(类似打电话)。TCP只能应用于双方进行通信,而广播与组播不能使用TCP协议。

    TCP协议适用于对传输质量要求较高,以及传输大量数据的通信。在需要可靠数据传输的场合通常使用TCP协议。

1.TCP协议的机制

在学习协议的具体内容前我们先学习TCP协议的机制 通过机制我们可以更好的学习TCP通信的通信步骤

(1)确认应答机制(收到数据就要应答)

在这里插入图片描述
主机A发送数据给主机B,每个数据都带了数据序号,主机B返回ACK应答
每一个ACK都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下一次你从哪里开始发 

作用:
1.保证安全:保证‘我’发送的消息,对方必须确认并恢复
2.保证多条数据确认信息的安全(告诉发送者,这次回应是对哪些数据,下次数据发送应该从什么时候开始)
 


(2)超时重传机制(安全机制)
超时重传机制触发:主机A发送数据给主机B,如果主机A在一个特定的时间间隔内没有收到来自主机B的确认应答,就会进行数据重发。

没有收到确认应答的情况:1.主机A的数据报在发送的过程中丢了。2.主机B的ACK应答丢了

超时时间的确定:TCP会根据当时的网络状态,动态的计算数据发送的速度,得到单次数据报发送的最大生存时间(MSL),超时时间即为(2MSL)

了解:如果一直接收不到ACK,超时时间会如何处理?
Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时时间都是500ms的整数倍.
如果重发一次之后, 仍然得不到应答, 等待 2500ms 后再进行重传.
如果仍然得不到应答, 等待 4500ms 进行重传. 依次类推, 以指数形式递增(2的指数倍).
累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接.
 


(3)连接管理机制(安全机制)

流程图:

在这里插入图片描述

 a)建立连接------>TCP三次握手

b)断开连接------>TCP四次挥手


(4)滑动窗口机制(效率)

这篇博客写的特别好学习TCP的滑动窗口机制可以看这篇博客:

TCP滑动窗口常见问题_SCUhzs-CSDN博客文章目录参考资料TCP滑动窗口概述引入窗口概念的原因窗口大小由哪一方决定?发送方的滑动窗口接收方的滑动窗口接收窗口和发送窗口的大小是相等的吗?TCP的可靠性,超时重传怎么实现滑动窗口如何实现面向流的可靠性?参考资料你还在为 TCP 重传、滑动窗口、流量控制、拥塞控制发愁吗?一文搞定!太厉害了,终于有人能把TCP/IP协议讲的明明白白了!TCP协议的滑动窗口具体是怎样控制流量的?TCP滑动窗口概述滑动窗口协议的基本原理就是:在任意时刻,发送方都维持了一个连续的允许发送的帧的序号,https://blog.csdn.net/qq_41996454/article/details/108651009
如果没有滑动窗口,网路数据传输就是串行的方式(发送一次之后,等待应答,这个时间内,主机A无事可做,主机B也一样),效率比较差。

使用滑动窗口可以解决效率的问题:类似于多线程的方式,并发的,同时发送多个数据报。
如下图:

在这里插入图片描述

窗口的实现实际上是操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除。

1.窗口大小指的是无需等待确认应答而可以继续发送数据的最大值. 上图的窗口大小就是4000个字节(四个段).
2.发送前四个段的时候, 不需要等待任何ACK, 直接发送;
3.收到第一个ACK后, 滑动窗口向后移动, 继续发送第五个段的数据 向后滑动; 依次类推(所以叫滑动窗口);
4.
操作系统内核为了维护这个滑动窗口, 需要开辟 发送缓冲区 来记录当前还有哪些数据没有应答;只有确认应答过的数据, 才能从缓冲区删掉(UDP就没有发送缓冲区)
5.窗口越大, 则网络的吞吐率就越高;

      发送端窗口随时间滑动图(不考虑重传)例如下所示:

        (1)我们一共需要发送900字节数据。可发送数据为1-500字节,尚未发送数据。假设首先发送400字节的数据。
        (2)发送了400字节后,对端返回一个ack表示收到200序号之内的数据且窗口通告为500。于是如图示,窗口向前滑动了200字节。当前已发送未确认字节序号为200-400,可发送字节序号为401-700,假设在此尚未发送数据。
        (3)对端返回一个ack表示收到400序号内的数据且窗口通告为400。于是如图示,窗口向前滑动了200字节。已确认数据序号为1-400,可发送数据为401-800。

窗口大小由哪一方决定?


TCP 头里有一个字段叫 Window,也就是窗口大小。

这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。

通常窗口的大小是由接收方的决定的。

发送方发送的数据大小不能超过接收方的窗口大小,否则接收方就无法正常接收到数据。
 

滑动窗口丢包问题:
1.数据报丢包

在这里插入图片描述

 如上图:如果主机A发送的数据报丢包,主机B的ack应答,会根据主机A已经收到的连续数据报的最大值+1返回ack应答,当主机A收到三个同样的ack应答之后,会将丢掉的数据报进行重发(具有接收缓冲区,来记录已经接收的数据报的序号)

2.ACK应答丢包(累计确认或者累计应答模式:这种情况下, 部分ACK丢了并不要紧, 因为可以通过后续的ACK进行确认
在这里插入图片描述

如果是滑动窗口的第一个包丢了,根据上述数据报丢包的情况,收到了第6个报的ACK应答,是从6001开始,说明第一个报主机B已经收到,所以ack丢包可以根据后序ack确定数据报主机B是否收到

关于滑动窗口的几个问题:
<1>.滑动窗口的大小:无需等待确认应答而可以继续发送数据的最大值
<2>.如何确定窗口的大小:由拥塞窗口和流量控制窗口决定(滑动窗口大小=(拥塞窗口大小,流量控制大小))(后序会讲到)
<3>.如何滑动:依赖于ACK的确认序号(ack确认序号前的数据报都已经接收到了),在该ACK确认序号前,当次并行收到了多少个数据报,就可以滑动多少
<4.>为什么要有接收缓冲区和发送缓冲区:
发送端的发送缓冲区:记录已经发送的数据——搜到对应的ACK应答,才可以清理该数据
接收端的接收缓冲区:记录已经接收的数据——如果发送数据报丢包,才知道让对方重发
 


(5)流量控制机制(安全机制)
接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送, 就会造成丢包, 继而引起丢包重传等等一系列连锁反应.

接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 “窗口大小” 字段, 通过ACK端通知发送端;
窗口大小字段越大, 说明网络的吞吐量越高;
接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;
发送端接受到这个窗口之后, 就会减慢自己的发送速度;
如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据
段, 使接收端把窗口大小告诉发送端.

当接收端使用流量控制窗口时,如何保证接受端的数据安全?
告诉发送端,影响发送端滑动窗口的大小


(6)拥塞控制机制(安全机制)
少量的丢包, 我们仅仅是触发超时重传; 大量的丢包, 我们就认为网络拥塞;

发送端在网络状态不明的情况下,贸然发送大量的数据,会造成网络拥堵,需要先发送少量数据探路,设置拥塞窗口的大小

在这里插入图片描述
如上图:如何确定拥塞窗口的大小
此处引入一个概念程为拥塞窗口
发送开始的时候, 定义拥塞窗口大小为1;
每次收到一个ACK应答, 拥塞窗口加1;
每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口;
为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍.
此处引入一个叫做慢启动的阈值
当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长


(7)延迟应答机制(效率)
举个例子:
假设接收端缓冲区为1M. 一次收到了500K的数据; 如果立刻应答, 返回的窗口就是500K;
但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了;
在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来;
如果接收端稍微等一会再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M;

延迟应答类型:
数量限制: 每隔N个包就应答一次;
时间限制: 超过最大延迟时间就应答一次;


(8)捎带机制(效率)
在延迟应答的基础上, 我们发现, 很多情况下, 客户端服务器在应用层也是 “一发一收” 的,意味着当客户端给服务端发送请求时,服务端会给客户端响应数据,此时ACK就像可以搭请求数据的顺风车,一起发送。

接收端响应的ACK,和主动发送的数据,可以合并返回。


2.TCP的特性总结


(1)TCP特性
TCP是有连接的可靠协议

在这里插入图片描述

(2)面向字节流
TCP既有发送缓冲区,也有接收缓冲区,数据没有大小限制

使用系统调用write时, 数据会先写入到内核的发送缓冲区中;
如果发送的字节数太长, 会被拆分成多个TCP的数据包发出;
如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去;
接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;
然后应用程序可以使用系统调用read从内核接收缓冲区拿数据;
另一方面, TCP的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可以写数据. 这个概念叫做 全双工

(3)粘包问题

【操作系统】为什么TCP会粘包、UDP不会粘包?_m0_46613023的博客-CSDN博客_为什么udp不会粘包TCP协议面向字节流发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。UDP协议面向数据报而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样.https://blog.csdn.net/m0_46613023/article/details/119951531

TCP和UDP 粘包详解_佛道教主-CSDN博客_udp粘包有关TCP和UDP 粘包 消息保护边界在socket网络程序中,TCP和UDP分别是面向连接和非面向连接的。TCP的socket编程,收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了https://blog.csdn.net/qq1263575666/article/details/79196762

要结合发送和接收缓冲区来考虑

1.TCP协议中虽然有seq的概念,但是这是在传输数据时的概念 确保接收到的数据是完整的 然后放到内核的接收缓冲区  。在内核的接收缓冲区中 多次或一次使用TCP协议传输的数据全部放在一起 全部都放在内核的接收缓冲区 ,所以应用程序在提取数据时不知道数据的边界 可以任意提取数据  就像数据流一样所以就是面向字节流 所以就会出现粘包。(这是接收方的粘包问题)

(发送方引起的粘包)是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。

2.UDP协议中,应用程序要从内核的接收缓冲区提取数据必须要以消息为单位,只能一个一个消息的提取 ,既面向数据报,所以UDP就不会出现粘包。

那么在内核的接收缓冲区是如何区分 一个一个消息的 ?

 对于UDP,不会使用块的合并优化算法,这样,实际上目前认为,是由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。(也就是说采用了类似于链表这样的结构来保存一个个消息 这样就有了消息边界)

发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。

在TCP的协议头中, 没有如同UDP一样的 “报文长度” 这样的字段, 但是有一个序号(seq)这样的字段
站在传输层角度看,报文是一个一个按照顺序排序好放在缓冲区,但是站在应用层角度看,都是一个个数字,不知道哪个数字是一段保文的开头,也不知道哪一个数字是结尾。这就是粘包
所以得明确一个报文的开头和结尾

但是对应UDP来说:
对于UDP, 如果还没有上层交付数据, UDP的报文长度仍然在. 同时, UDP是一个一个把数据交付给应用层.就有很明确的数据边界.
站在应用层的站在应用层的角度, 使用UDP的时候, 要么收到完整的UDP报文, 要么不收. 不会出现"半个"的情况

而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。
怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,UDP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。

例如基于TCP的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束。

所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。

此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
 

一个socket的两端,都会有send和recv两个方法(或者使用write和read),如client发送数据到server,那么就是客户端进程调用send发送数据,而send(write())的作用是将数据拷贝进入socket的内核发送缓冲区之中,然后send便会在上层返回。

也就是说send()方法返回之时,数据不一定会发送到对端即服务器上去(和write写文件有点类似),send()(write())仅仅是把应用层buffer的数据拷贝进socket的内核发送buffer中,发送是TCP的事情,和send其实没有太大关系。

接收缓冲区把数据缓存入内核,等待recv()读取,recv()所做的工作,就是把内核缓冲区中的数据拷贝到应用层用户的buffer里面,并返回。若应用进程一直没有调用recv()进行读取的话,此数据会一直缓存在相应socket的接收缓冲区内。
 


3.TCP报头格式

TCP数据封装在一个IP数据报中(如下图),TCP报头有其固定格式,如果不计可选项字段,通常为20个字节。

//TCP报头格式

在这里插入图片描述

 六位标志位:
URG: 紧急指针是否有效(0或者1)
ACK: 确认序号是否有效(0或者1)
PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走
RST: 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段
SYN: 请求建立连接; 我们把携带SYN标识的称为同步报文段(0或者1)
FIN: 通知对方, 本端要关闭了, 我们称携带FIN标识的为结束报文段(0或者1)

seq:上图中的序号TCP连接中传送的字节流中的每个字节都按顺序编号,第一个字节的编号由本地随机产生
重点掌握seq ,ACK,SYN,FIN

报文内的具体字段的含义如下:

    1.源端口号 与 目的端口号:

        每个TCP段都包含源端和目的端的端口号,用于定位接收和发送应用进程数据。端口号是由本地操作系统分配的,在主机内部是唯一的。部分为TCP应用程序连接的保留端口如下:

        HTTP        80

        Telnet        23

        FTP            20,21

        NetBIOS会话    139

        HTTPS        443
 

  2.序号(seq):

        序号用来标识从TCP发送端向TCP接收端发送的数据字节流的第一个字节的序号。在TCP传送的流中,每一个字节为一个序号。

        例如,当前报文的序号为300,当前报文的数据共100字节,则下一个期望报文段的序号应为400。序号确保TCP传输的有序性。

        序号为32位无符号整数,计数范围为0~2^31-1,当达到最大数值后又从0开始。

    3.确认序号(ack)

        确认序号用来标识希望收到的下一个数据报的序号。只有当标志位ACK为1时,确认序号才有效。

    4.TCP报头长度:

        TCP报头长度给出当前的报头中32位字的数目。使用该字段是因为TCP报头有可选项,因此报头长度不固定。若无可选项字段,该字段数值为5。

    5.预留:

        未使用的预留6位,为未来扩充TCP报头标志位预留空间,一般值为0。

    6.标志位:

        在TCP报头中有6个标志位,每个标志位的含义如下:

            URG        紧急指针有效

            ACK        ACK为1时表示确认序号有效;ACK为0时则确认序号被忽略

            PSH        PSH为1时表示该报文段应立即递交给应用程序而不是在缓冲区排队等待

            RST        重建TCP连接标志,用于当主机崩溃等未知原因而出现错误连接时重建TCP连接

            SYN        同步序号,用于发起一个TCP连接

            FIN        FIN为1时表示发送端已没有数据传输,此时会关闭TCP连接

    7.窗口大小:

        接收端用于告知发送端该接收端的缓冲区大小,以此控制发送速率,从而达到流量控制。

    8.校验和:

        该字段用于对发送数据进行数据校验,这是一个强制性字段,一定是由发送端计算并存储,并由接收端进行验证。

    9.紧急指针:

        只有URG标志位为1时紧急指针字段才有效。紧急指针是一个正的偏移量,与序号字段的值相加表示紧急数据最后一个字节的序号。


三次握手(建立TCP连接)

三次握手四次挥手重点其实就是前面学习TCP协议的特性中的 确认应答机制

TCP连接中传送的字节流中的每个字节都按顺序编号,第一个字节的编号由本地随机产生

seq:

seq其实就是这个报文段中的第一个字节的数据编号。

例如,一段报文的序号字段值是 200 ,而携带的数据共有100字节,显然下一个报文段(如果还有的话)的数据序号应该从300开始(结合滑动窗口中的段使用的);

序号(seq)(通常结合滑动窗口)用来标识从TCP发端向TCP收端发送的数据字节流,它表示在这个报文段中的的第一个数据字节。如果将字节流看作在两个应用程序间的单向流动,则TCP用序号对每个字节进行计数。序号是32bit的无符号数,序号到达232-1后又从0开始。

当建立一个新的连接时,SYN标志变1。序号字段包含由这个主机选择的该连接的初始序号ISN(InitialSequenceNumber)。该主机要发送数据的第一个字节序号为这个ISN加1,因为SYN标志消耗了一个序号。既然每个传输的字节都被计数,确认序号包含发送确认的一端所期望收到的下一个序号。因此,确认序号(ack)应当是上次已成功收到数据字节序号加1。只有ACK标志(下面介绍)为1时确认序号字段才有效。

发送ACK无需任何代价,因为32bit的确认序号字段(ack)和ACK标志一样,总是TCP首部的一部分。因此,我们看到一旦一个连接建立起来,这个字段总是被设置,ACK标志也总是被设置为1。

TCP为应用层提供全双工服务。这意味数据能在两个方向上独立地进行传输。因此,连接的每一端必须保持每个方向上的传输数据序号。

注意ACK和ack的区别:

ACK:ACK的值代表ack是否有效,ACK通常为0或1

ack:是确认序号 确认序号用来标识希望收到的下一个数据报的序号。只有当标志位ACK为1时,确认序号才有效。

TCP是一个面向连接的可靠传输协议,通信双方在发送数据前必须建立一个TCP连接。为了建立一个TCP连接,通常需要三步操作:

  • 第一次握手:客户端将标志位SYN置为1,随机产生一个值seq=J(序号),并将该数据包发送给服务器端,客户端进入SYN_SENT状态,等待服务器端确认。
  • 第二次握手:服务器端收到数据包后由标志位SYN=1知道客户端请求建立连接,服务器端将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给客户端以确认连接请求,服务器端进入SYN_RCVD状态。
  • 第三次握手:客户端收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给服务器端,服务器端检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,客户端和服务器端进入ESTABLISHED状态,完成三次握手,随后客户端与服务器端之间可以开始传输数据了。

注意:这里服务器和客户端都会给对方发送seq(序号,值为随机)  如果数据传输没有差错,随后给对方的ack(确认序号)的值就是seq的+1这样就可以确保数据没有丢失。其实就是确认机制。

同时握手:

  •  我们同时称呼A和B为Client,他们都执行主动打开的操作(Active Opener)。
  • 同时两端的状态变化都是由CLOSED->SYN_SENT->SYN_RCVD->ESTABLISHED。
  • 建立连接的时候需要四个数据包的交换,并且每个数据包中都携带有SYN标识,直到收到SYN的ACK为止同时关闭连接
     

 什么是 TCP 半连接队列和全连接队列?
在 TCP 三次握手的时候,Linux 内核会维护两个队列,分别是:

  • 半连接队列,也称 SYN 队列;
  • 全连接队列,也称 accepet 队列;

服务端收到客户端发起的 SYN 请求后,内核会把该连接存储到半连接队列,并向客户端响应 SYN+ACK,接着客户端会返回 ACK,服务端收到第三次握手的 ACK 后,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其添加到 accept 队列,等待进程调用 accept 函数时把连接取出来。

不管是半连接队列还是全连接队列,都有最大长度限制,超过限制时,内核会直接丢弃,或返回 RST 包。

四次挥手(解除连接)

建立一个TCP连接需要三次握手,但是释放一个TCP连接需要四次挥手,这是由于TCP连接的半关闭(half-close)特性造成的。

何为半关闭?

因为按理来说 我发送了关闭(FIN)后应该不再发送或者接收任何数据了。但是:
TCP提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力。这就是所谓的半关闭。

怎么使用半关闭?
为了使用这个特性,编程接口必须为应用程序提供一种方式来说明“
我已经完成了数据 传送,因此发送一个文件结束( F I N)给另一端,但我还想接收另一端发来的数据,直到它给 我发来文件结束( F I N) ” 。也就是下图中的客户端在发送FIN 请求断开连接后服务器端也进行了应答,但是此时TCP连接还没有真正的关闭,直到服务器端给客户端发送FIN请求断开连接 且客户端对此进行应答。
 

释放一个TCP连接通常需要四步操作(如下图),可以发现和三次握手的区别 三次握手的SYN可以和ACK一起发送 但是四次挥手的FIN不是和ACK一起发送的

第一张图写的是FIN=M 应该是简略写法 意思应该是将FIN置1,seq=M 可以通过前面的标志位的学习中得知

  • 中断连接端可以是客户端,也可以是服务器端。
  • 第一次挥手:客户端发送一个FIN=1,seq=M,用来关闭客户端到服务器端的数据传送,客户端进入FIN_WAIT_1状态。意思是说"我客户端没有数据要发给你了",但是如果你服务器端还有数据没有发送完成,则不必急着关闭连接,可以继续发送数据。
  • 第二次挥手:服务器端收到FIN和seq后,先发送ack=M+1,告诉客户端,你的请求我收到了,但是我还没准备好,请继续你等我的消息。这个时候客户端就进入FIN_WAIT_2 状态,继续等待服务器端的FIN报文。
  • 第三次挥手:当服务器端确定数据已发送完成,则向客户端发送FIN=1,seq=N报文,告诉客户端,好了,我这边数据发完了,准备好关闭连接了。服务器端进入LAST_ACK状态。
  • 第四次挥手:客户端收到FIN=1,seq=N报文后,就知道可以关闭连接了,但是他还是不相信网络,怕服务器端不知道要关闭,所以发送ack=N+1后进入TIME_WAIT状态,如果Server端没有收到ACK则可以重传。服务器端收到ACK后,就知道可以断开连接了。客户端等待了2MSL后依然没有收到回复,则证明服务器端已正常关闭,那好,我客户端也可以关闭连接了。最终完成了四次握手。

 上面是一方主动关闭,另一方被动关闭的情况,实际中还会出现同时发起主动关闭的情况,

同时挥手具体流程如下图:

太厉害了,终于有人能把TCP/IP 协议讲的明明白白了

 思考:TCP 是面向字节流的协议,UDP 是面向报文的协议?这里该如何理解?_-JAVA高级架构的博客-CSDN博客_tcp是字节流udp是报文格式有个读者问我,这么个问题:TCP 是面向字节流的协议,UDP 是面向报文的协议?这里的「面向字节流」和「面向报文」该如何理解。#如何理解字节流?之所以会说 TCP 是面向字节流的协议,UDP 是面向报文的协议,是因为操作系统对 TCP 和 UDP 协议的发送方的机制不同,也就是问题原因在发送方。先来说说为什么 UDP 是面向报文的协议?当用户消息通过 UDP 协议传输时,操作系统不会对消息进行拆分,在组装好 UDP 头部后就交给网络层来处理,所以发出去的 UDP 报文中的数据部分https://blog.csdn.net/JAVA88866/article/details/124773380

(1)为什么三次握手的SYN可以和ack一起发送?四次挥手的FIN要和ack分开发?不可以一 起发送吗?

答:可能一起发送,但也可能不一起发送。不一起发送是因为可能服务器端还有数据要发送给客户端不能马上结束TCP连接,所以服务器端在接受到来自客户端的FIN请求后回复ack表示接受到了结束请求,但是由于可能还要继续发送未发送完的数据,所以要在数据发送完成后 由服务器端发送FIN请求给客户端 正式结束连接。

(2)四次挥手也可能变成三次挥手

如果被动关闭端调用 close/shutdown 函数非常及时,内核在很大概率上也会将 ACK 与 FIN 放在一个报文中发送,这就变成三次挥手了。

(3)关闭TCP连接一定需要4次挥手吗? 
不一定,4次挥手关闭TCP连接是最安全的做法。但在有些时候,我们不喜欢TIME_WAIT 状态(如当MSL数值设置过大导致服务器端有太多TIME_WAIT状态的TCP连接,减少这些条目数可以更快地关闭连接,为新连接释放更多资源),这时我们可以通过设置SOCKET变量的SO_LINGER标志来避免SOCKET在close()之后进入TIME_WAIT状态,这时将通过发送RST强制终止TCP连接(取代正常的TCP四次握手的终止方式)。但这并不是一个很好的主意,TIME_WAIT 对于我们来说往往是有利的。

 常见面试题
【问题1】为什么连接的时候是三次握手,关闭的时候却是四次握手?

答:因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。

【问题2】为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?(也就是四次挥手的图中 客户端最后一次发送ACK后要等待2MSL的时间 是为了防止这个ACK丢失)

答:虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。在Client发送出最后的ACK回复,但该ACK可能丢失。Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到了该ACK。Client会在发送出ACK之后进入到TIME_WAIT状态。Client会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。所谓的2MSL是两倍的MSL(Maximum Segment Lifetime)。MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。

【问题3】为什么不能用两次握手进行连接?

答:3次握手完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已准备好),也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认。

       现在把三次握手改成仅需要两次握手,死锁是可能发生的。作为例子,考虑计算机S和C之间的通信,假定C给S发送一个连接请求分组,S收到了这个分组,并发 送了确认应答分组。按照两次握手的协定,S认为连接已经成功地建立了,可以开始发送数据分组。可是,C在S的应答分组在传输中被丢失的情况下,将不知道S 是否已准备好,不知道S建立什么样的序列号,C甚至怀疑S是否收到自己的连接请求分组。在这种情况下,C认为连接还未建立成功,将忽略S发来的任何数据分 组,只等待连接确认应答分组。而S在发出的分组超时后,重复发送同样的分组。这样就形成了死锁。

【问题4】如果已经建立了连接,但是客户端突然出现故障了怎么办?

TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。
 


UDP协议

简介
UDP(User Datagram Protocol,用户数据报协议),一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务,UDP在IP报文的协议号是17。常用的UDP端口号有:53(DNS)、69(TFTP)、161(SNMP),使用UDP协议包括:TFTP、SNMP、NFS、DNS、BOOTP、CoAP、DTLS。

    与TCP不同,传输层的另一种协议UDP是一个简单的面向数据报的传输层协议(TCP是面向连接),其在内核没有发送缓冲区,只有接收缓冲区

与TCP协议最大的不同在于UDP是一种不可靠传输协议,即它发送数据到IP层的数据报,但不保证这些数据报到达其目的地。

    由于UDP通信之前无需建立一个连接,因此UDP应用要比TCP应用更加简单,效率上比TCP也更加高效。

所以如果是视频数据这种使用UDP更好,如果是命令数据这种使用TCP更好

    UDP数据封装在一个IP数据报中,UDP报头有其固定格式,如果不计可选项字段,通常为8个字节。

  • 源端口号:在需要对方回信时选用,不需要时可用全0
  • 目的端口号:这在终点交付报文时必须使用
  • 长度:UDP用户数据报的总长度(首部+数据),其最小值是8(仅有首部)。
  • 检验和:检测UDP用户数据报在传输中是否有错,有错就丢弃

报文内的具体字段的含义如下:

    1.源端口号 与 目的端口号:

    端口号标识出发送进程和接收进程。部分为UDP应用程序连接的保留端口如下:

        NetBIOS名称服务    137

        SNMP            161

        DNS                53

        RIP路由选择协议    521

    2.UDP长度:

    以字节为单位的UPD数据和UDP报头之和,最小值为8。该UDP长度是冗余的,IP数据报含有其总长度。

    3.UPD校验和:

    覆盖UDP报头和UDP数据,用于数据校验。

如果接收方发现收到的报文中的目的端口号不正确(即不存在对应于该端口号的应用进程),就丢弃该报文,并由网际控制报文协议ICMP发送“端口不可达”差错报文给发送方。我们在ICMP的应用举例讨论 traceroute时,就是让发送的UDP用户数据报故意使用一个非法的UDP端口,结果ICMP就返回“端口不可达”差错报文因而达到了测试的目的。


关于UDP数据报

调用一次 recvfrom 函数只会读取到一个数据报的数据

UDP 是一种无连接的协议,UDP 数据报是独立的,它们之间没有关联。当发送方调用 sendto 发送 UDP 数据报时,操作系统将其封装成数据报,并把它交给网络层发送。在接收方,操作系统将接收到的 UDP 数据报拆开,并将其传递给应用层。这个过程中,操作系统并不保证所有的数据报都被完整地接收,也不保证它们会按照发送的顺序到达。

因此,在调用 recvfrom 时,我们不能保证能够一次性读取到完整的数据报。如果数据报长度超过了缓冲区的大小,那么就会被截断;如果数据报还没有接收完,那么只能读取到已经接收到的部分。因此,每次调用 recvfrom 函数只能读取到一个数据报的数据。如果需要读取多个数据报,就需要多次调用 recvfrom 函数。

总结:

udp是基于数据报的:

1.使用 sendto 函数发送 UDP 数据时,每次调用该函数都代表发送一个数据报!UDP 是一种无连接协议,它不会像 TCP 一样维护连接状态,因此每个数据报都是独立的,需要单独发送。在使用 sendto 函数发送数据时,需要指定目标主机的 IP 地址和端口号,这就构成了一个完整的 UDP 报文。如果要发送多个数据报,则需要多次调用 sendto 函数。(这也是为什么sendto接口需要sockaddr_in结构体作为参数)

2.调用 一次recvform 函数时,操作系统会从内核的UDP接收缓冲区读取一个数据报。

如果读取到的这个数据报大小大于我们指定的接收数据缓冲区可以接受的大小(也就是recvfrom()的第三个参数)就会发生数据截断 丢弃数据包后面的内容。如果希望在接收端一次性读取多个数据报,可以使用循环来调用recvfrom函数,并根据需要将接收到的数据报保存到缓冲区中。但请注意,这种方法仅适用于数据报的大小已知且相同的情况。在其他情况下,可能需要使用其他技术,如流传输或自定义协议来解决问题。

UDP数据报由什么组成?它的大小如何决定?

UDP数据报由报头和数据两部分组成。报头包含了UDP的源端口号、目的端口号、UDP数据报长度以及校验和等控制信息;数据则是应用程序发送的实际数据。在UDP协议中,每个UDP数据报都是一个完整的数据单元,因此每个UDP数据报的大小是固定的。

UDP数据报的大小由发送端应用程序决定。发送端应用程序将要发送的数据按照UDP数据报的格式组织好后,可以通过调用sendto()函数将数据报发送出去。在发送数据报之前,应用程序需要确定数据报的大小,以便内核分配足够的空间来存储该数据报。在Linux内核中,UDP数据报的最大大小由MTU(最大传输单元)决定,通常为1500字节。如果数据报的大小超过了MTU,则数据报会被分片,每个分片都会成为一个独立的UDP数据报进行传输!

值得注意的是

UDP是不粘包的,粘包指的是数据没有边界;而UDP的数据是有边界的,UDP传输是以数据报作为基本单位的,所以理论上是不粘包的。UDP“粘包”-CSDN博客


UDP特点

  • UDP是无连接的,减少开销和发送数据之前的时延
  • UDP使用最大努力交付,即不保证可靠交付
  • UDP是面向报文的,适合一次性传输少量数据的网络应用
  • UDP无拥塞控制,适合很多实时应用
  • 首部开销小,仅8个字节(TCP首部为20个字节)


1.无连接:没有建立连接就发数据
2.不可靠:没有类似TCP保证数据传输的安全机制,(连接管理机制,确认应答机制,超时机制
,)效率更高

3.面向数据报:只能一次接收(系统级别的操作:调用系统函数)
4.没有发送缓冲区(发了消息就不管),有接收缓冲区
5.UDP传输数据最大为64k

在这里插入图片描述
发送缓冲区:

主机1发送完数据,发出之后就不管了


接收缓冲区:
如果发送端调用一次sendto, 发送100个字节, 那么接收端也必须调用对应的一次recvfrom, 接收100个字节; 而不能循环调用10次recvfrom, 每次接收10个字节;
所以,接收数据的时候,发送100个字节,系统读取只调用一次,但是可以读取多次发来的其他100字节。
但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致; 如果缓冲区满了, 再到达的UDP数据就会被丢弃;

如何使用UDP进行可靠传输?

  • 引入序列号, 保证数据顺序;
  • 引入确认应答, 确保对端收到了数据;
  • 引入超时重传, 如果隔一段时间没有应答, 就重发数据;


 TCP和UDP常见面试题

网络 卧槽!牛皮了,面试官居然把TCP三次握手四次挥手问的这么详细_WhiteShirtI的博客-CSDN博客一、为什么握手是三次,而不是两次或者四次?答:两次不安全,四次没必要。tcp通信需要确保双方都具有数据收发的能力,因此双方都要发送SYN确保对方具有通信的能力二、为什么挥手是四次而不是三次?答:发送FIN包只能表示对方不再发送数据了,不代表对方不再接收数据,因此被动关闭方进行ACK回复之后有可能还会继续发送数据,等到不再发送数据了才会发送下一个FIN包,因此FIN包和ACK包是分开的...https://blog.csdn.net/qq_44443986/article/details/115966274

<1>.说一说TCP/IP模型,以及都做了哪些事情
TCP/IP模型分为五层,分别是应用层,传输层,网络层,数据链路层,物理层

TCP/IP协议群主要是报文的拆分,增加协议头,数据的传输,路由和寻址以及数据的重组

<2>.说一说TCP的三次握手四次挥手
1.建立连接------>TCP三次握手:

TCP------>三次握手的流程

1.主机A发送syn到主机B,要求建立a到b的连接。此时主机A的状态为syn_sent
2.主机B回复ack+syn(这里的ack和syn数据报本来是两个,但是仅标志位不同,所以可以合并,为什么不是四次的原因),要求建立b到a的连接,主机B的状态为syn_rcvd
3.主机A回复第2步syn的ack。主机A的状态为established,建立A到B的连接
主机B接收到第3步的数据报,建立B到A 的连接,主机B的状态置为established

TCP------>三次握手中的问题:
1.syn为什么有两个?
双方的连接状态会持续,且连接是有方向的

2.第二步中,为什么是ack+syn?
本质上是一个发ack应答,一个发syn请求,而且是方向一致的两个数据报,可以合并

3.第三步中,ack确认应答哪个?
应答第二步的syn

2.断开连接------>TCP四次挥手:

TCP------>四次挥手的流程
1.主机A发送fin到主机B,请求关闭a到b的连接
2.主机B回复ack,主机B的状态置为close_wait
3.主机B发送fin到主机A,请求关闭b到a的连接
4.值即A回复ack(第三步的fin),状态置为time_wait
主机B接收到第四步的数据报,状态置为closed
主机A经过2MSL(超时等待时间)之后,状态置为closed

TCP------>4次挥手中的问题
1.第2步和第3步为什么不能和3次握手流程一样,进行合并
原因:第2步是TCP协议在系统内核中实现时,自动响应的ack
第3步时应用程序手动调用close来关闭连接的
程序在关闭连接之前,可能需要执行释放资源等前置操作,所以不能合并(TCP协议实现时,没有这样进行设计)

2.第3步中,主机A为什么不能直接设置为closed状态
原因: 第4个数据报可能丢包,如果直接置为closed,丢包后无法重新发送数据。
主机B达到超时时间之后,会重发第三个数据报,然后要求主机A再次回复ack

3.服务器出现大量的close_wait状态,是为什么?
服务端没有正确的关闭连接(程序没有调用close,或者没有正确使用)
 

<3>TCP和UDP的区别

1.TCP是有连接的可靠传输协议,而UDP是无连接的
2.UDP传时数据是有大小限制的,而TCP没有
3.UDP是面向数据报的,而TCP是面向数据流的。
4.TCP保证数据正确性,顺序性,而UDP不能保证.

5.UDP的传输速率高于TCP

<4>如何选择使用TCP协议还是UDP协议?

1.数据可靠性

对数据可靠性要求高时,必须选择TCP协议;而对数据可靠性要求并不高时,可以选择UDP协议。

2.实时性

由于TCP协议中存在三次握手、四次挥手、重传数据等保证数据可靠的手段,因此使用TCP协议会有较大的延迟。若对数据实时性要求较高,则应选用UDP协议。

3.网络可靠性

在网络状况不是很好时,需选用TCP协议保证数据传输稳定;

而在网络状况很好的情况下,应选择UDP协议减少网络负荷。

<5>如何用UDP进行可靠传输

引入序列号, 保证数据顺序;
引入确认应答, 确保对端收到了数据;
引入超时重传, 如果隔一段时间没有应答, 就重发数据;


面向字节流(TCP)和面向报文(UDP)

1.TCP面向字节流

面向字节流的话,虽然应用程序和TCP的交互是一次一个数据块(大小不等),但TCP把应用程序看成是一连串的无结构的字节流。TCP有一个缓冲,当应用程序传送的数据块太长,TCP就可以把它划分短一些再传送。如果应用程序一次只发送一个字节,TCP也可以等待积累有足够多的字节后再构成报文段发送出去。

打个比方比喻TCP,你家里有个蓄水池,你可以里面倒水,蓄水池上有个龙头,你可以通过龙头将水池里的水放出来,然后用各种各样的容器装(杯子、矿泉水瓶、锅碗瓢盆)接水。
上面的例子中,往水池里倒几次水和接几次水是没有必然联系的,也就是说你可以只倒一次水,然后分10次接完。另外,水池里的水接多少就会少多少;往里面倒多少水,就会增加多少水,但是不能超过水池的容量,多出的水会溢出。

结合TCP的概念,水池就好比接收缓存,倒水就相当于发送数据,接水就相当于读取数据。好比你通过TCP连接给另一端发送数据,你 只调用了一次write,发送了100个字节,但是对方可以分10次收完,每次10个字节;你也可以调用10次write,每次10个字节,但是对方可以一次就收完(可以这样操作是因为内核中有接收缓冲区)。(假设数据都能到达)但是,你发送的数据量不能大于对方的接收缓存(流量控制),如果你硬是要发送过量数据,则对方的缓存满了就会把多出的数据丢弃。

2.UDP面向报文

UDP和TCP不同,发送端调用了几次write,接收端必须用相同次数的read读完。UPD是基于报文的,在接收的时候,每次最多只能读取一个报文,报文和报文是不会合并的,如果内核的接收缓冲区小于报文长度,则多出的部分会被丢弃。也就说,如果不指定MSG_PEEK标志,每次读取操作将消耗一个报文。

面向报文的传输方式是应用层交给UDP多长的报文,UDP就照样发送,即一次发送一个报文。因此,应用程序必须选择合适大小的报文。若报文太长,则IP层需要分片,降低效率。若太短,会是IP太小。UDP对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。这也就是说,应用层交给UDP多长的报文,UDP就照样发送,即一次发送一个报文。

原因:

其实,这种不同是由TCP和UDP的特性决定的。TCP是面向连接的,也就是说,在连接持续的过程中,socket中收到的数据都是由同一台主机发出的(劫持什么的不考虑),因此,知道保证数据是有序的到达就行了,至于每次读取多少数据自己看着办。

而UDP是无连接的协议,也就是说,只要知道接收端的IP和端口,且网络是可达的,任何主机都可以向接收端发送数据。这时候,如果一次能读取超过一个报文的数据,则会乱套。比如,主机A向发送了报文P1,主机B发送了报文P2,如果能够读取超过一个报文的数据,那么就会将P1和P2的数据合并在了一起,这样的数据是没有意义的。


SOCKET(套接字)

Socket原理讲解_Tony-jiang的博客-CSDN博客_socket对TCP/IP、UDP、Socket编程这些词你不会很陌生吧?随着网络技术的发展,这些词充斥着我们的耳朵。那么我想问:1.什么是TCP/IP、UDP?2.Socket在哪里呢?3.Socket是什么呢?4.你会使用它们吗?什么是TCP/IP、UDP?TCP/IP(Transmission Cont...https://blog.csdn.net/pashanhu6402/article/details/96428887

我们常说的应用层协议的实现实际上是对于SOCKET接口的封装吗?

是的,应用层协议的实现通常是基于Socket套接字接口进行封装的。在使用Socket套接字时,应用程序需要按照特定的协议进行编程,以便正确地发送和接收数据。而应用层协议的实现则是在Socket套接字的基础上,进一步封装和抽象,以提供更高层次的功能和服务。例如,HTTP协议就是基于Socket套接字实现的,而HTTP协议的实现则是在Socket套接字的基础上,进一步封装了HTTP请求和响应的格式、状态码、报头等信息,以提供Web服务。因此,可以说应用层协议的实现实际上是对于Socket接口的封装。

套接字(Socket)是什么呢?

        在网络通信中,套接字是成对出现的。

        就好像插板和插头 这样一一对应 以进行通信

有几个概念需要在开头澄清一下

TCP socket分两种,监听socket和传输socket两种

监听socket:负责处理网络上来的连接请求(客户端的syn包到达便是连接请求来了,如果不知道syn包,请参看一下TCP三次握手);

传输socket:负责在网络上的两个端点之间传输TCP数据。(需要成对出现)

       Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

套接字(socket)

    1982年,BSD(Berkeley Software Distribution,伯克利软件套件)引入了套接字机制。套接字初期作为进程间通信的一种手段。1986年,伯克利扩展了socket的接口,使之与TCP/IP协议匹配,因此套接字成为了现在的操作系统内主流的网络通信手段。

//socket为何会被翻译为“套接字”已无从考证。socket的原始含义是硬件上的插座。伯克利将socket的概念引入进程间通信,可能是想表达进程通信像插插座一样,一方“插入”,一方“被插入”。1984年,socket的概念引入国内,自此开始socket被翻译为“套接字”。至于为何译者将socket翻译为套接字已是一个未解之谜。

套接字(socket)是一种特殊的I/O接口,在代码内体现为特殊的文件描述符。socket是一种常见的进程间通信的手段,不仅可以实现本地进程间通信,更可以配合TCP/IP协议实现网络通信。

在OSI体系结构中套接字位于会话层与传输层之间的位置,在TCP/IP体系结构中套接字位于应用层与传输层之间的位置,每一个套接字都由{协议、地址、端口}三部分来表示。

 

套接字的类型主要有三种:

    1.流式套接字(SOCK_STREAM)

    流式套接字提供可靠的、面向连接的通信流(TCP面向字节流),保证数据传输可靠性和有序性。TCP通信使用该类型套接字。

    2.数据报套接字(SOCK_DGRAM)

    数据报套接字提供不可靠的、无连接的通信流(UDP面向数据报),数据通过相互独立的报文进行传输,是无序的并且不保证可靠传输。UDP通信使用该类型套接字。

    3.原始套接字(SOCK_RAW)

    原始套接字允许对底层协议(IP或ICMP等)进行直接访问,虽然功能强大但使用不便。

Socket是什么呢?
       Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
你会使用它们吗?
       前人已经给我们做了好多的事了,网络间的通信也就简单了许多,但毕竟还是有挺多工作要做的。以前听到Socket编程,觉得它是比较高深的编程知识,但是只要弄清Socket编程的工作原理,神秘的面纱也就揭开了。
       一个生活中的场景。你要打电话给一个朋友,先拨号,朋友听到电话铃声后提起电话,这时你和你的朋友就建立起了连接,就可以讲话了。等交流结束,挂断电话结束此次交谈。    生活中的场景就解释了这工作原理,也许TCP/IP协议族就是诞生于生活中,这也不一定。


预备知识

网络字节序

我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分,那么如何定义网络数据流的地址呢?发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。

TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。例如上一节的UDP段格式,地址0-1是16位的源端口号,如果这个端口号是1000(0x3e8),则地址0是0x03,地址1是0xe8,也就是先发0x03,再发0xe8,这16位在发送主机的缓冲区中也应该是低地址存0x03,高地址存0xe8。但是,如果发送主机是小端字节序的,这16位被解释成0xe803,而不是1000。因此,发送主机把1000填到发送缓冲区之前需要做字节序的转换。同样地,接收主机如果是小端字节序的,接到16位的源端口号也要做字节序的转换。如果主机是大端字节序的,发送和接收都不需要做转换。同理,32位的IP地址也要考虑网络字节序和主机字节序的问题。

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换


大小端转换函数:

#include <arpa/inet.h>

uint32_t htonl(uint32_t hostlong);

uint16_t htons(uint16_t hostshort);

uint32_t ntohl(uint32_t netlong);

uint16_t ntohs(uint16_t netshort);

h表示host(表示主机字节序),n表示network(表示网络字节序)!!!

l表示32位长整数(一般用于IP地址),s表示16位短整数(一般用于端口号)。

如果主机是小端字节序,这些函数将参数转换为大端换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

网络字节序:

小端法:(pc本地存储) 高位存高地址。地位存低地址。

大端法:(网络存储) 高位存低地址。地位存高地址。

htonl --> 本地--》网络 (IP) 192.168.1.11 --> string --> atoi --> int --> htonl --> 网络字节序

htons --> 本地--》网络 (port)

ntohl --> 网络--》 本地(IP)

ntohs --> 网络--》 本地(Port)


IP地址转换函数

下面两个函数是点分十进制的ip地址和网络传输二进制ip地址之间的转换函数

#include <arpa/inet.h>

1.int inet_pton(int af, const char *src, void *dst);

//这个函数转换字符串ip 为网络字节序ip,第一个参数af是地址族,转换后存在dst中

参数:

        af:AF_INET(ipv4)、AF_INET6(ipv6)

        src:传入参数,IP地址(点分十进制)

        dst:传出参数,转换后的 网络字节序的 IP地址。

返回值:

        成功: 1

        异常: 0, 说明src指向的不是一个有效的ip地址。

        失败:-1

2.const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

//这个函数转换网络二进制结构到ASCII类型的地址,参数的作用和上面相同,只是多了一个参数socklen_t cnt,他是所指向缓存区dst的大小,避免溢出,如果缓存区太小无法存储地址的值,则返回一个空指针,并将errno置为ENOSPC

参数:

        af:AF_INET(ipv4)、AF_INET6(ipv6)

        src: 网络字节序IP地址

        dst:本地字节序(string IP)

        size: dst 的大小。

返回值:

         成功:dst(既指向点分十进制ip的指针)。

        失败:NULL

这两个函数支持IPv4和IPv6且是可重入函数

但是上面的两个函数使用起来有点麻烦还是偷懒吧 使用下面的函数:

inet_pton()和inet_ntop()函数详解_QvQ是惊喜不是哭泣的博客-CSDN博客_inet_ntop函数ip地址的转化https://blog.csdn.net/zyy617532750/article/details/58595700

1.把ip地址转化为用于网络传输的二进制数值
int inet_aton(const char *cp, struct in_addr *inp);

inet_aton() 转换网络主机地址ip(如192.168.1.10)为二进制数值,并存储在struct in_addr结构中,即第二个参数*inp,函数返回非0表示cp主机有地有效,返回0表示主机地址无效。(这个转换完后不能用于网络传输,还需要调用htons或htonl函数才能将主机字节顺序转化为网络字节顺序)

in_addr_t inet_addr(const char *cp);

inet_addr函数转换网络主机地址(如192.168.1.10)为网络字节序二进制值,如果参数char *cp无效,函数返回-1(INADDR_NONE),这个函数在处理地址为255.255.255.255时也返回-1,255.255.255.255是一个有效的地址,不过inet_addr无法处理;


2.将网络传输的二进制数值转化为成点分十进制的ip地址

char *inet_ntoa(struct in_addr in);

注意参数:参数应该是struct sockaddr_in 的sin_addr成员

举个例子:        pTmp = inet_ntoa(client_addr.sin_addr);

inet_ntoa 函数转换网络字节排序的地址为标准的ASCII以点分开的地址,该函数返回指向点分开的字符串地址(如192.168.1.10)的指针,该字符串的空间为静态分配的,这意味着在第二次调用该函数时,上一次调用将会被重写(复盖),所以如果需要保存该串最后复制出来自己管理!
 


sockaddr数据结构

strcut sockaddr 很多网络编程函数诞生早于IPv4协议,那时候都使用的是sockaddr结构体,为了向前兼容,现在sockaddr退化成了(void *)的作用,传递一个地址给函数,至于这个函数是sockaddr_in还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。

sockaddr数据结构

上图中的struct sockaddr已经不用了,如果是ipv4就使用struct sockaddr_in

如果是ipv6就使用struct sockaddr_in6

struct sockaddr {

sa_family_t sa_family; /* address family, AF_xxx */

char sa_data[14]; /* 14 bytes of protocol address */

};

使用 sudo grep -r "struct sockaddr_in {"  /usr 命令可查看到struct sockaddr_in结构体的定义。一般其默认的存储位置:/usr/include/linux/in.h 文件中。

ipv4:

struct sockaddr_in {

__kernel_sa_family_t sin_family; /* Address family */   地址结构类型

__be16 sin_port;   /* Port number */ 端口号

struct in_addr sin_addr; /* Internet address */ IP地址

/* Pad to size of `struct sockaddr'. */

unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -

sizeof(unsigned short int) - sizeof(struct in_addr)];

};

struct in_addr { /* Internet address. */

__be32 s_addr;

};

ipv6 

struct sockaddr_in6 {

unsigned short int sin6_family; /* AF_INET6 */

__be16 sin6_port; /* Transport layer port # */

__be32 sin6_flowinfo; /* IPv6 flow information */

struct in6_addr sin6_addr; /* IPv6 address */

__u32 sin6_scope_id; /* scope id (new in RFC2553) */

};

struct in6_addr {

union {

__u8 u6_addr8[16];

__be16 u6_addr16[8];

__be32 u6_addr32[4];

} in6_u;

#define s6_addr in6_u.u6_addr8

#define s6_addr16 in6_u.u6_addr16

#define s6_addr32   in6_u.u6_addr32

};



#define UNIX_PATH_MAX 108

struct sockaddr_un {

__kernel_sa_family_t sun_family; /* AF_UNIX */

char sun_path[UNIX_PATH_MAX]; /* pathname */

};


****************************************************************************************************

Pv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位端口号和32位IP地址,IPv6地址用sockaddr_in6结构体表示,包括16位端口号、128位IP地址和一些控制字段。UNIX Domain Socket的地址格式定义在sys/un.h中,用sock-addr_un结构体表示。各种socket地址结构体的开头都是相同的,前16位表示整个结构体的长度(并不是所有UNIX的实现都有长度字段,如Linux就没有),后16位表示地址类型。IPv4、IPv6和Unix Domain Socket的地址类型分别定义为常数AF_INET、AF_INET6、AF_UNIX。这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。因此,socket API可以接受各种类型的sockaddr结构体指针做参数,例如bind、accept、connect等函数,这些函数的参数应该设计成void *类型以便接受各种类型的指针,但是sock API的实现早于ANSI C标准化,那时还没有void *类型,因此这些函数的参数都用struct sockaddr *类型表示,在传递参数之前要强制类型转换一下,例如:

struct sockaddr_in servaddr;

bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr)); /* initialize servaddr */

****************************************************************************************************


值得注意的事

以TCP为例子:

   先从服务器端说起。服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。

既所以我们想使用TCP和UDP协议进行通信 就得使用socket接口

TCP socket分两种,监听socket和传输socket两种

监听socket:负责处理网络上来的连接请求(客户端的syn包到达便是连接请求来了,如果不知道syn包,请参看一下TCP三次握手);

传输socket:负责在网络上的两个端点之间传输TCP数据。(需要成对出现)

       Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

****需要注意的地方:

上图中的TCP通信流程一共存在三个套接字;其中 一个用来监听,两个用来通信

其中服务器端使用socket()创建的套接字 是为了监听的既目的是创建监听socket 所以后面使用了bind( )和listen( )为其服务(所以在服务器端我们使用bind传入的是服务器端的 ip和端口号 目的是将监听套接字和指定的IP和端口绑定 以便用来监听网络中的连接请求) 在我们正式通过三次握手建立了连接时accept()会返回一个套接字 这个套接字是传输套接字 和我们客户端使用socket( )创建的传输套接字相对应 为一对传输套接字

 ***********************************************************************************************************

TCP网络编程中connect()、listen()和accept()三者之间的关系 ( 非常重要!!)_EminentBoy的博客-CSDN博客_网络编程listenhttps://blog.csdn.net/tennysonsky/article/details/45621341 基于 TCP 的网络编程开发分为服务器端和客户端两部分,常见的核心步骤和流程如下:连接详情: connect()函数对于客户端的 connect() 函数,该函数的功能为客户端主动连接服务器,建立连接是通过三次握手,而这个连接的过程是由内核完成,不是...https://blog.csdn.net/qq_20398345/article/details/81132207************************************************************************************************************

1.  listen()函数的作用(设定监听模式(被动模式)):

因为使用socket()默认创建的套接字 都是具有主动属性(既主动与别人连接)的 使用listen( )可以使将其变为被动属性(被动与别人连接,既等待别人和我连接)

如 上图中的服务器端使用了listen()函数的作用是服务器端程序需要调用listen()系统调用将socket状态由TCP_CLOSE迁移到TCP_LISTEN,这样该套接字才能处理来自客户端的SYN请求。(既设定监听模式 相当于设置一个参数,不要以为它的作用是阻塞到服务器端接受到 客户端的SYN为止,这是错误的,listen()是不会阻塞的 )


int listen(int sockfd, int backlog)函数的主要作用就是将套接字( sockfd )变成被动的连接监听套接字(被动等待客户端的连接),至于参数 backlog 的作用是设置内核中连接队列的长度(这个长度有什么用,后面做详细的解释),TCP 三次握手也不是由这个函数完成,listen()的作用仅仅告诉内核一些信息。

这里需要注意的是listen()函数不会阻塞,它主要做的事情为,将该套接字和套接字对应的连接队列长度告诉 Linux 内核,然后,listen()函数就结束。

这样的话,当有一个客户端主动连接(connect()),Linux 内核就自动完成TCP 三次握手,将建立好的链接自动存储到队列中,如此重复。

所以,只要 TCP 服务器调用了 listen(),客户端就可以通过 connect() 和服务器建立连接,而这个连接的过程是由内核完成。

三次握手的连接队列

这里详细的介绍一下 listen() 函数的第二个参数( backlog)的作用:告诉内核连接队列的长度

为了更好的理解 backlog 参数,我们必须认识到:

内核为任何一个给定的监听套接字维护两个队列:

1、未完成连接队列(incomplete connection queue),每个这样的 SYN 分节对应其中一项:已由某个客户发出并到达服务器,而服务器正在等待完成相应的 TCP 三次握手过程。这些套接口处于 SYN_RCVD 状态。


2、已完成连接队列(completed connection queue),每个已完成 TCP 三次握手过程的客户对应其中一项。这些套接口处于 ESTABLISHED 状态。

当来自客户的 SYN 到达时,TCP 在未完成连接队列中创建一个新项,然后响应以三次握手的第二个分节:服务器的 SYN 响应,其中稍带对客户 SYN 的 ACK(即SYN+ACK),这一项一直保留在未完成连接队列中,直到三次握手的第三个分节(客户对服务器 SYN 的 ACK )到达或者该项超时为止(曾经源自Berkeley的实现为这些未完成连接的项设置的超时值为75秒)。

如果三次握手正常完成,该项就从未完成连接队列移到已完成连接队列的队尾

backlog 参数历史上被定义为上面两个队列的大小之和,大多数实现默认值为 5,当服务器把这个完成连接队列的某个连接取走后,这个队列的位置又空出一个,这样来回实现动态平衡,但在高并发 web 服务器中此值显然不够。

  什么是 TCP 半连接队列和全连接队列?
在 TCP 三次握手的时候,Linux 内核会维护两个队列,分别是:

  • 半连接队列,也称 SYN 队列;
  • 全连接队列,也称 accepet 队列;

服务端收到客户端发起的 SYN 请求后,内核会把该连接存储到半连接队列,并向客户端响应 SYN+ACK,接着客户端会返回 ACK,服务端收到第三次握手的 ACK 后,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其添加到 accept 队列,等待进程调用 accept 函数时把连接取出来。

不管是半连接队列还是全连接队列,都有最大长度限制,超过限制时,内核会直接丢弃,或返回 RST 包。

2.accept()函数的作用

 accept()函数功能是,从处于 established 状态的连接队列(已完成连接队列)头部取出一个已经完成的连接,如果这个队列没有已经完成的连接,accept()函数就会阻塞,直到取出队列中已完成的用户连接为止。

如果,服务器不能及时调用 accept() 取走队列中已完成的连接,队列满掉后会怎样呢?UNP(《unix网络编程》)告诉我们,服务器的连接队列满掉后,服务器不会对再对建立新连接的syn进行应答,所以客户端的 connect 就会返回 ETIMEDOUT。但实际上Linux的并不是这样的!


结合socket编程来分析TCP的三次握手和四次挥手

这是结合socket编程API角度的三次握手:

下面是socket编程API角度的四次挥手,我们可以知道在编程中体现四次挥手是:1.第一次挥手:客户端调用了close(socketfd)主动关闭传输套接字。这就相当于主动发起了四次挥手,也就是发送了FIN给服务器端,这是第一次挥手。 2.第二次挥手:而服务器端,我们前面学习知道read读取socket套接字的时候如果返回0 就代表客户端断开连接。也就是代表服务器端接收到了客户端发来的FIN,同时向客户端返回ack,这是第二次挥手。3.第三次挥手:服务器端在执行完数据传输后 调用close(socketfd)关闭传输套接字,就相当于服务器端发送了FIN给客户端,这是第三次挥手。4.第四次挥手:客户端发送ack给服务器端,然后马上进入TIME-WAIT状态,这在代码上没有体现(我们调用setsockopt配置SO_REUSEADDR选项就是为了解决TIME-WAIT引发的问题),这是第四次挥手。 

 

 好好理解上面三次握手和四次挥手在socket编程中的体现,这样可以对于我们学习socket编程可以有帮助。比如通过上面的两个图就可以知道:如果我们的客户端不是调用close断开连接,而是异常断开连接这时候服务器端的read就会返回-1 表示客户端不是正常断开连接的 因为如果客户端正常调用close断开连接的话 服务器端的read会返回0. 因此要好好理解上面的结和socket编程API的三次握手和四次挥手的图,从而进一步的理解为什么代码要那样写,都是层层递进的。

 上面这个图就是完整的TCP连接、传输数据、断开连接的流程 好好看上面这个图!


TCP编程

 使用socket接口进行TCP协议通信的流程如图

****需要注意的地方:

上图中的TCP通信流程一共存在三个套接字;其中 一个用来监听,两个用来通信

其中服务器端使用socket()创建的套接字 是为了监听的既目的是创建监听socket 所以后面使用了bind( )和listen( )为其服务 在我们正式通过三次握手建立了连接时accept()会返回一个套接字 这个套接字是传输套接字 和我们客户端使用socket( )创建的传输套接字相对应 为一对传输套接字

一个socket的两端,都会有send和recv两个方法(或者使用write()和read()),如client发送数据到server,那么就是客户端进程调用send发送数据而send(write())的作用是将数据拷贝进入socket的内核发送缓冲区之中,然后send便会在上层返回。

也就是说send()方法返回之时,数据不一定会发送到对端即服务器上去(和write写文件有点类似),send()(write())仅仅是把应用层buffer的数据拷贝进socket的内核发送buffer中,发送是TCP的事情,和send其实没有太大关系。

接收缓冲区把数据缓存入内核,等待recv()读取,recv()所做的工作,就是把内核缓冲区中的数据拷贝到应用层用户的buffer里面,并返回。若应用进程一直没有调用recv()进行读取的话,此数据会一直缓存在相应socket的接收缓冲区内。

**所以在经过三次握手建立了TCP连接后 如果服务器端或者客户端要给对方发送数据 调用send()或者write()

如上图(步骤其实都是根据TCP协议流程来的)

1)如果需要使用TCP协议创建一个服务器,则需要以下步骤:

    -创建套接字(socket())

    -绑定套接字(bind())

    -设定监听模式(listen())

    -接收客户机的连接请求(accept())

    -接收/发送数据

    -断开连接

2)与服务器端类似,如果需要使用TCP协议创建一个访问TCP服务器的客户机,需要以下步骤:

    -创建套接字

    -绑定套接字(可选)

    -向服务器发送连接请求

    -接收/发送数据

    -断开连接


TCP编程接口函数

read()、write()、send()、recv()这两组接口只适用于TCP协议的编程!

sendto()和recvfrom()这组接口则是TCP或者UDP都可以使用 所以为了养成习惯避免记混 建议是不管是UDP还是TCP都使用sendto()和recvfrom()这组接口!

1、创建一个套接字


在网络通信中通常使用套接字socket进行通信。若需要进行网络通信则必须创建一个套接字。使用socket()函数创建一个套接字。

socket()函数的用法:

    函数socket()

    所需头文件:#include<sys/types.h>

                        #include<sys/socket.h>

    函数原型:int socket(int domain, int type, int protocol)

    函数参数:

        domain        选择通信协议。通信协议族定义在头文件socket.h中。常用的协议有:

            AF_INET        IPv4通信协议

            AF_INET6    IPv6通信协议

            AF_UNIX        本地通信

            //其他取值见man帮助文档和头文件socket.h内容

        type        套接字类型。常见的值有:

            SOCK_STREAM    流式套接字,TCP协议选用该套接字类型

            SOCK_DGRAM    数据报套接字,UDP协议选用该套接字类型

            SOCK_RAW    原始套接字,IP/ICMP协议选用该套接字类型

        protocol    协议值,通常情况下为0,即默认IP协议;其他协议值通过查看:/user/include/linux/in.h

    函数返回值:

        成功:非负套接字文件描述符

        失败:-1

使用socket( )创建的套接字具有主动属性。如果需要被动属性(监听模式)需要使用listen()

    使用scoket()创建套接字是网络编程的第一步,也是最重要的一步,后续的所有网络通信编程函数都是基于套接字进行操作的。

    当调用socket()之后,内核内部生成一个socket结构体,并返回该结构体的文件描述符作为返回值以便后续代码进行操作。

注意:套接字在Linux内核中是一个结构十分复杂的结构体,使用socket()创建一个套接字会在内核里生成一个套接字结构体并返回该套接字结构体的文件描述符。因此绝对不可以使用未获得socket()返回值的普通的int类型变量进行后续编程操作。
 

2、绑定套接字


    使用socket()创建套接字后,该套接字虽然存在但并未给它分配地址,还需将该套接字与指定的IP地址和端口号进行绑定。使用bind()给套接字分配地址(IP地址和端口号)

bind()函数的用法:

    函数bind()

    所需头文件:#include<sys/types.h>

                        #include<sys/socket.h>

    函数原型:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)

    函数参数:

       1. sockfd   。 需要绑定的套接字的文件描述符

       2. addr    绑定的地址通常绑定的是调用bind()方的ip和端口号因为这个参数有 const所以是传入参数 该参数是一个结构体指针,必须使用地址传递的方式传参。需要针对不同的协议设定不同的结构体类型,常见的结构体如下:

sin_port和sin_addr都必须是网络字节序(NBO),一般可视化的数字都是主机字节序(HBO)。

struct sockaddr    //通用地址结构体;具体情况下自己还要进行替换

            {

                sa_family_t sa_family;    //地址族,值为AF_XXX

                char sa_data[14];    //协议地址

            }

            

struct sockaddr_in    //IPv4协议地址结构体,这个才是我们要用到的结构体

            {

                sa_family_t sin_family;    //地址族,固定值AF_INET, IPv4

                in_port_t sin_port;    //端口号,通常使用htons()计算获得

                struct in_addr sin_addr;    //IP地址,具体结构体见下

            }

            在结构体sockaddr_in中,第三个成员sin_addr是一个in_addr类型的结构体,其结构体如下:

struct in_addr

                {

                    uint32_t s_addr;    //网络字节序 IP地址,为32位无符号数,通常使用inet_addr()计算获得 也经常使用 INADDR_ANY      这个参数表示网络中任意的ip

                }

       3. addrlen    地址长度。值为第二个参数addr的结构体长度,通常为sizeof(sockaddr_in)

    函数返回值:

        成功:0

        失败:-1

    绑定套接字函数bind()将套接字与需要进行网络通信的地址信息建立连接。服务器端必须进行bind()操作,但客户端可以不手动进行bind(),等到通信连接后客户端可以自动进行bind()操作。
 

3、设置监听模式


使用listen()将套接字标记为被动模式,即作为accept()请求连接接入的套接字。

listen()函数的用法:

    函数listen()

    所需头文件:#include<sys/types.h>

                        #include<sys/socket.h>

    函数原型:int listen(int sockfd, int backlog)

    函数参数:

        sockfd      设定监听的套接字的文件描述符,该套接字必须为SOCK_STREAM类型或SOCK_SEQPACKET类型

        backlog    请求队列的最大请求数。若该队列已满,则客户端会收到ECONNREFUSED错误,以便通知客户端在后续重试连接。该值默认为5。

    函数返回值:

        成功:0

        失败:-1

完成监听后,服务器会暂时阻塞,等待客户端发来连接请求(由客户端的connect()函数发送),并使用accept()响应该请求。
 

4、响应连接请求


服务器端使用accept()函数响应客户端的连接请求。

accept()函数的用法:

    函数accept()

    所需头文件:#include<sys/types.h>

                        #include<sys/socket.h>

    函数原型:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)

    函数参数:

        sockfd    套接字文件描述符,该套接字必须由socket()创建,bind()绑定,listen()设置监听

        addr       保存客户端的地址信息,结构体类型见bind()第二个参数。这个参数和bind()中的地址信息参数的不同之处在于 :

这里的地址信息参数没有const ,为传出参数; 保存和服务器端建立连接一方的IP和端口号,既在客户端连接后用来保存客户端的IP和端口号

                必须使用地址传递的方式传参,或设定为NULL。若设定为NULL则表示不保存客户端地址信息,并且第三个参数addrlen也必须为NULL

        addrlen    保存客户端addr结构体的长度,单位为字节,必须使用地址传递的方式传参。若第二个参数addr设定为NULL则该参数也必须为NULL(这是传入传出参数 既客户端addr结构体大小)

    函数返回值:

        成功:建立好连接的套接字文件描述符(为服务器端的通信套接字,和客户端创建的通信套接字相对应 为一对)

        失败:-1


accept()函数功能是,从处于 established 状态的连接队列(已完成连接队列)头部取出一个已经完成的连接,如果这个队列没有已经完成的连接,accept()函数就会阻塞,直到取出队列中已完成的用户连接为止。

一个监听套接字可以监听多个连接,也就是说如果服务器要和多个客户端连接 使用一次Listen( )就可以了;但是因为建立TCP连接通信双方的传输套接字需要成对出现,所以要多次调用accept( ) 来创建多个传输套接字 ,并且返回多个传输socket描述符。我们就可以分别用这些传输socket描述符 使用read和write来分别和多个客户端来进行数据传输!!!!!

比如:

 

5、向服务器发送连接请求


客户端使用connect()向TCP服务器发送连接请求,服务器端使用accept()接收该请求

函数connect()的用法:

    函数connect()

    所需头文件:#include<sys/types.h>

                        #include<sys/socket.h>

    函数原型:int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)

    函数参数:

        sockfd    需要发送数据的套接字文件描述符

        addr    服务器端地址信息(IP和端口号),必须使用地址传递传参。因为是连接函数所以这个参数就是要连接IP和端口号,也就是服务器端的IP和端口号

        addrlen    值为第二个参数addr的结构体长度,通常为sizeof(sockaddr_in)

    函数返回值:

        成功:0

        失败:-1

以上就是TCP编程需要的所有系统调用函数。除了这些之外,还需要其他的一些函数。
 

6、接收数据与发送数据


在使用socket()、bind()、listen()和accept()后,服务器端与客户端已经成功的建立了通信,此时可以使用send()向对方发送数据,使用recv()接收对方发送的数据。

//除了send()/recv(),有时也可以使用read()/write()来接收和发送数据。其中write()可以取代send(),read()可以取代recv()。使用send()/recv()还是read()/write()全凭程序员个人习惯,但是通篇代码使用要统一。

send()函数的用法:

    函数send()

    所需头文件:#include<sys/types.h>

                        #include<sys/socket.h>

    函数原型:int send(int sockfd, const void *buf, size_t len, int flags)

    函数参数:

        sockfd    需要发送数据的套接字文件描述符

        buf        发送数据的缓冲区首地址

        len        发送数据的缓冲区长度

        flags     通常设定为0。若flags设定为0,则send()与write()等价。

    函数返回值:

        成功:实际发送的字节数

        失败:-1

recv()函数的用法:

    函数recv()

    所需头文件:#include<sys/types.h>

                        #include<sys/socket.h>

    函数原型:int recv(int sockfd, void *buf, size_t len, int flags)

    函数参数:

        sockfd    需要接收数据的套接字文件描述符

        buf         接收数据的缓冲区首地址

        len          接收数据的缓冲区长度

        flags       通常设定为0。若flags设定为0,则recv()与read()等价。

    函数返回值:

        成功:实际接收的字节数

        失败:-1

若调用recv()后暂时未收到对方send()的数据,则recv()会阻塞等待直至数据到达,如果发送方关闭recv()会立刻返回0。
 


实例:

//服务器端
 
//文件server.c    
 
#include<stdio.h>
 
#include<sys/types.h>
 
#include<sys/socket.h>
 
#include<stdlib.h>
 
#include<string.h>
 
#include<unistd.h>
 
#include<arpa/inet.h>
 
#define BUFFER 128
 
int main(int argc, const char *argv[])
 
{
 
    int listenfd,connfd;//通信用套接字,其中listenfd用于服务器本身创建连接用套接字,connfd用于accept()函数返回用套接字
 
    struct sockaddr_in servaddr,cliaddr;//保存地址与端口号结构体,其中servaddr用于服务器信息,cliaddr用于accept()传参保存客户机信息
 
    //由于使用IPv4方式,所以直接定义sockaddr_in类型结构体,在传参时直接进行强制类型转换即可
 
    socklen_t peerlen;//结构体大小
 
    char buf[BUFFER];//保存信息缓冲区
 
    if(argc<3)
 
    {
 
        printf("too few argument\n");
 
        printf("Usage: %s <ip> <port>\n",argv[1]);
 
        exit(0);
 
    }
 
    /*1.创建套接字socket()*/
 
    if((listenfd=socket(AF_INET,SOCK_STREAM,0))<0)
 
    {
 
        perror("socket");
 
        exit(0);
 
    }
 
    printf("listenfd is %d\n",listenfd);//创建成功
 
    /*1.5.设定servaddr成员,准备绑定bind()*/
 
    bzero(&servaddr,sizeof(servaddr));
 
    servaddr.sin_family = AF_INET;//协议:IPv4
 
    servaddr.sin_port = htons(atoi(argv[2]));//端口号,由argv[2]给定
 
    servaddr.sin_addr.s_addr = inet_addr(argv[1]);//IP地址:由argv[1]给定
 
    /*2.绑定套接字bind()*/
 
    if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)//注意强制类型转换与地址传递
 
    {
 
        perror("bind");
 
        exit(0);
 
    }
 
    printf("bind success!\n");//绑定成功
 
    /*3.设定监听listen()*/
 
    if(listen(listenfd,5)<0)
 
    {
 
        perror("listen");
 
        exit(0);
 
    }
 
    printf("Listening...\n");//监听成功
 
    peerlen = sizeof(cliaddr);
 
    while(1)//使用循环,让服务器不会立即退出
 
    {
 
    /*4.使用accept()接收客户机连接请求*/
 
        if((connfd=accept(listenfd,(struct sockaddr*)&cliaddr,&peerlen))<0)
 
        {
 
            perror("accept");
 
            exit(0);
 
        }
 
        memset(buf,0,sizeof(buf));
 
    /*5.使用recv()接收客户机信息*/
 
        if(recv(connfd,buf,BUFFER,0)<0)
 
        {
 
            perror("recv");
 
            exit(0);
 
        }
 
        printf("Received a message:%s\n",buf);//接收成功并打印
 
        strcpy(buf,"Welcome to server");
 
    /*6.使用send()向客户机发送信息*/
 
        send(connfd,buf,BUFFER,0);
 
    /*7.关闭连接*/
 
        close(connfd);
 
    }
 
    close(listenfd);
 
    return 0;
 
}
//客户端
 
//文件client.c
 
#include<stdio.h>
 
#include<sys/types.h>
 
#include<sys/socket.h>
 
#include<stdlib.h>
 
#include<string.h>
 
#include<unistd.h>
 
#include<arpa/inet.h>
 
#define BUFFER 128
 
int main(int argc, const char *argv[])
 
{
 
    int sockfd;//通信用套接字
 
    char buf[BUFFER]="Hello World";//保存信息缓冲区
 
    struct sockaddr_in myaddr;//保存地址与端口号结构体
 
    if(argc<3)
 
    {
 
        printf("too few argument\n");
 
        printf("Usage: %s <ip> <port>\n",argv[0]);
 
        exit(0);
 
    }
 
    /*1.创建套接字socket()*/
 
    if((sockfd = socket(AF_INET,SOCK_STREAM,0))<0)
 
    {
 
        perror("socket");
 
        exit(0);
 
    }
 
    /*1.5.设定myaddr成员*/
 
    bzero(&myaddr,sizeof(myaddr));
 
    myaddr.sin_family = AF_INET;
 
    myaddr.sin_port = htons(atoi(argv[2]));
 
    myaddr.sin_addr.s_addr = inet_addr(argv[1]);
 
    /*2.使用connect()与服务器进行连接*/
 
    if(connect(sockfd,(struct sockaddr*)&myaddr,sizeof(myaddr))<0)//若未开启服务器,则这步会报错
 
    {
 
        perror("connect");
 
        exit(0);
 
    }
 
    /*3.使用send()发送信息*/
 
    send(sockfd,buf,sizeof(buf),0);
 
    /*4.使用recv()接收服务器信息*/
 
    if(recv(sockfd,buf,sizeof(buf),0)<0)
 
    {
 
        perror("recv");
 
        exit(0);
 
    }
 
    printf("recv from server: %s\n",buf);
 
    /*5.关闭连接*/
 
    close(sockfd);
 
    return 0;
 
}

练习1:使用TCP协议连接服务器端与客户端,客户端可以不断向服务器端传输信息,直至输入特定信息(例如"byebye")服务器才会与客户端断开连接

答案:

//文件server.c
 
#include<stdio.h>
 
#include<sys/types.h>
 
#include<sys/socket.h>
 
#include<stdlib.h>
 
#include<string.h>
 
#include<unistd.h>
 
#include<arpa/inet.h>
 
#define BUFFER 128
 
int main(int argc, const char *argv[])
 
{
 
    int listenfd,connfd;
 
    struct sockaddr_in servaddr,cliaddr;
 
    socklen_t peerlen;
 
    char buf[BUFFER];
 
    if(argc<3)
 
    {
 
        printf("too few argument\n");
 
        printf("Usage: %s <ip> <port>\n",argv[1]);
 
        exit(0);
 
    }
 
    if((listenfd=socket(AF_INET,SOCK_STREAM,0))<0)
 
    {
 
        perror("socket");
 
        exit(0);
 
    }
 
    printf("listenfd is %d\n",listenfd);
 
    bzero(&servaddr,sizeof(servaddr));
 
    servaddr.sin_family = AF_INET;
 
    servaddr.sin_port = htons(atoi(argv[2]));
 
    servaddr.sin_addr.s_addr = inet_addr(argv[1]);
 
    if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
 
    {
 
        perror("bind");
 
        exit(0);
 
    }
 
    printf("bind success!\n");
 
    if(listen(listenfd,5)<0)
 
    {
 
        perror("listen");
 
        exit(0);
 
    }
 
    printf("Listening...\n");
 
    peerlen = sizeof(cliaddr);
 
    if((connfd=accept(listenfd,(struct sockaddr*)&cliaddr,&peerlen))<0)
 
    {
 
        perror("accept");
 
        exit(0);
 
    }
 
    printf("Connect:%d\n",connfd);
 
    while(1)
 
    {
 
        memset(buf,0,sizeof(buf));
 
        if(recv(connfd,buf,BUFFER,0)<0)
 
        {
 
            perror("recv");
 
            exit(0);
 
        }
 
        printf("Received a message:%s",buf);
 
        //printf("[%s:%d] %s",inet_ntoa(cliaddr.sin_addr),ntohs(cliaddr.sin_port),buf);
 
        if(strncmp(buf,"byebye",6)==0)//指定断开连接信息为"byebye"
 
        {
 
            printf("Disconnect:%d\n",connfd);
 
            close(connfd);
 
            break;
 
        }
 
    }
 
    close(listenfd);
 
    return 0;
 
}
//文件client.c
 
#include<stdio.h>
 
#include<sys/types.h>
 
#include<sys/socket.h>
 
#include<stdlib.h>
 
#include<string.h>
 
#include<unistd.h>
 
#include<arpa/inet.h>
 
#define BUFFER 128
 
int main(int argc, const char *argv[])
 
{
 
    int sockfd;
 
    char buf[BUFFER];
 
    struct sockaddr_in myaddr;
 
    if(argc<3)
 
    {
 
        printf("too few argument\n");
 
        printf("Usage: %s <ip> <port>\n",argv[0]);
 
        exit(0);
 
    }
 
    if((sockfd = socket(AF_INET,SOCK_STREAM,0))<0)
 
    {
 
        perror("socket");
 
        exit(0);
 
    }
 
    bzero(&myaddr,sizeof(myaddr));
 
    myaddr.sin_family = AF_INET;
 
    myaddr.sin_port = htons(atoi(argv[2]));
 
    myaddr.sin_addr.s_addr = inet_addr(argv[1]);
 
    if(connect(sockfd,(struct sockaddr*)&myaddr,sizeof(myaddr))<0)
 
    {
 
        perror("connect");
 
        exit(0);
 
    }
 
    while(1)
 
    {
 
        memset(buf,0,sizeof(buf));
 
        fgets(buf,BUFFER,stdin);
 
        send(sockfd,buf,sizeof(buf),0);
 
        if(strncmp(buf,"byebye",6)==0)//指定断开连接信息为"byebye"
 
        {
 
            printf("Client will exit\n");
 
            close(sockfd);
 
            break;
 
        }
 
    }
 
    return 0;
 
}

运行这个练习代码时,可以尝试不打开服务器端,直接运行数据端发送数据。可以发现客户端是无法启动的(Connection refused访问被拒绝)。

这是因为TCP协议是可靠连接,在通信之前必须在服务器端与客户端建立可靠连接。

//练习2:练习文件IO+网络编程

练习2:在服务器端有一个存放成绩的文件score.txt

学号    姓名    成绩

10101    Liu        98

10102    Zhao    97

10103    Sun        92

10104    Chen    97

10105    Tian    95

10106    Li        99

10107    Xiao    96

10108    Meng    94

10109    Lion    91

10110    Bunk    97

客户端向服务器端发送学号,服务器端查找文件内容并返回结果。若有该学号信息,则返回成绩;若无该学号信息,则返回#代表查找失败

答案:
 

//文件server.c
 
#include<stdio.h>
 
#include<sys/types.h>
 
#include<sys/socket.h>
 
#include<stdlib.h>
 
#include<string.h>
 
#include<unistd.h>
 
#include<arpa/inet.h>
 
#define BUFFER 128
 
int main(int argc, const char *argv[])
 
{
 
    int listenfd,connfd;
 
    FILE *scorefd;
 
    struct sockaddr_in servaddr,cliaddr;
 
    socklen_t peerlen;
 
    int flag = 0;
 
    char buf[BUFFER],recvbuf[BUFFER]={0};
 
    if(argc<3)
 
    {
 
        printf("too few argument\n");
 
        printf("Usage: %s <ip> <port>\n",argv[1]);
 
        exit(0);
 
    }
 
    if((scorefd=fopen("score.txt","r+"))==NULL)
 
    {
 
        perror("cannot open score.txt");
 
        exit(0);
 
    }
 
    if((listenfd=socket(AF_INET,SOCK_STREAM,0))<0)
 
    {
 
        perror("socket");
 
        exit(0);
 
    }
 
    printf("listenfd is %d\n",listenfd);
 
    bzero(&servaddr,sizeof(servaddr));
 
    servaddr.sin_family = AF_INET;
 
    servaddr.sin_port = htons(atoi(argv[2]));
 
    servaddr.sin_addr.s_addr = inet_addr(argv[1]);
 
    if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
 
    {
 
        perror("bind");
 
        exit(0);
 
    }
 
    printf("bind success!\n");
 
    if(listen(listenfd,5)<0)
 
    {
 
        perror("listen");
 
        exit(0);
 
    }
 
    printf("Listening...\n");
 
    peerlen = sizeof(cliaddr);
 
    if((connfd=accept(listenfd,(struct sockaddr*)&cliaddr,&peerlen))<0)
 
    {
 
        perror("accept");
 
        exit(0);
 
    }
 
    printf("Connect:%d\n",connfd);
 
    memset(buf,0,sizeof(buf));
 
    if(recv(connfd,recvbuf,BUFFER,0)<0)
 
    {
 
        perror("recv");
 
        exit(0);
 
    }
 
    printf("Received a number:%s",recvbuf);
 
    while(fgets(buf,BUFFER,scorefd)!=NULL)//查询文件内容
 
    {
 
        if(strncmp(buf,recvbuf,5)==0)//如果发现了目标学号循环停止
 
        {
 
            flag = 1;
 
            break;
 
        }
 
    }
 
    if(flag==1)//找到
 
    {
 
        send(connfd,buf,sizeof(buf),0);//发送给客户端
 
    }
 
    else//未找到
 
    {
 
        memset(buf,0,sizeof(buf));
 
        sprintf(buf,"#");
 
        send(connfd,buf,sizeof(buf),0);//给客户端发送#表示查找失败
 
    }
 
    close(connfd);
 
    close(listenfd);
 
    fclose(scorefd);
 
    return 0;
 
}

//文件client.c
 
#include<stdio.h>
 
#include<sys/types.h>
 
#include<sys/socket.h>
 
#include<stdlib.h>
 
#include<string.h>
 
#include<unistd.h>
 
#include<arpa/inet.h>
 
#define BUFFER 128
 
int main(int argc, const char *argv[])
 
{
 
    int sockfd;
 
    char buf[BUFFER];
 
    struct sockaddr_in myaddr;
 
    if(argc<3)
 
    {
 
        printf("too few argument\n");
 
        printf("Usage: %s <ip> <port>\n",argv[0]);
 
        exit(0);
 
    }
 
    if((sockfd = socket(AF_INET,SOCK_STREAM,0))<0)
 
    {
 
        perror("socket");
 
        exit(0);
 
    }
 
    bzero(&myaddr,sizeof(myaddr));
 
    myaddr.sin_family = AF_INET;
 
    myaddr.sin_port = htons(atoi(argv[2]));
 
    myaddr.sin_addr.s_addr = inet_addr(argv[1]);
 
    if(connect(sockfd,(struct sockaddr*)&myaddr,sizeof(myaddr))<0)
 
    {
 
        perror("connect");
 
        exit(0);
 
    }
 
    memset(buf,0,sizeof(buf));
 
    printf("Please input number:");
 
    fgets(buf,BUFFER,stdin);
 
    send(sockfd,buf,sizeof(buf),0);
 
    memset(buf,0,sizeof(buf));
 
    if(recv(sockfd,buf,sizeof(buf),0)<0)
 
    {
 
        perror("recv");
 
        exit(0);
 
    }
 
    if(strncmp("#",buf,1)!=0)
 
    {
 
        printf("recv from server:\n%s",buf);
 
    }
 
    else
 
    {
 
        printf("No result!\n");
 
    }
 
    return 0;
 
}

//练习3:练习线程编程+网络编程

练习3:使用多线程编程,实现服务器端与客户端的双向通信,即服务器端与客户端可以同时接收和发送数据。当客户端输入"byebye"时服务器端与客户端断开连接

//注意:本题使用进程编程需要进程通信手段,代码难度大幅度上升,因此不推荐使用进程编程
 
//服务器端
 
//文件server_thread.c
 
//注意:由于服务器接收到"byebye"后需要与客户端断开连接,因此在主进程内接收数据而在线程内发送数据
 
#include<stdio.h>
 
#include<sys/types.h>
 
#include<sys/socket.h>
 
#include<stdlib.h>
 
#include<string.h>
 
#include<unistd.h>
 
#include<pthread.h>
 
#include<arpa/inet.h>
 
#define BUFFER 128
 
void *thread_send(void *arg)
 
{
 
    int newsockfd = *((int*)arg);
 
    char sendbuffer[BUFFER]={0};
 
    while(1)
 
    {
 
        fgets(sendbuffer,BUFFER,stdin);
 
        send(newsockfd,sendbuffer,BUFFER,0);
 
    }
 
    pthread_exit(NULL);//可以省略
 
}
 
int main(int argc, const char *argv[])
 
{
 
    int listenfd,connfd;
 
    struct sockaddr_in servaddr,cliaddr;
 
    socklen_t peerlen;
 
    char buf[BUFFER];
 
    pthread_t sendtid;
 
    if(argc<3)
 
    {
 
        printf("too few argument\n");
 
        printf("Usage: %s <ip> <port>\n",argv[1]);
 
        exit(0);
 
    }
 
    if((listenfd=socket(AF_INET,SOCK_STREAM,0))<0)
 
    {
 
        perror("socket");
 
        exit(0);
 
    }
 
    printf("listenfd is %d\n",listenfd);
 
    bzero(&servaddr,sizeof(servaddr));
 
    servaddr.sin_family = AF_INET;
 
    servaddr.sin_port = htons(atoi(argv[2]));
 
    servaddr.sin_addr.s_addr = inet_addr(argv[1]);
 
    if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
 
    {
 
        perror("bind");
 
        exit(0);
 
    }
 
    printf("bind success!\n");
 
    if(listen(listenfd,10)<0)
 
    {
 
        perror("listen");
 
        exit(0);
 
    }
 
    printf("Listening...\n");
 
    peerlen = sizeof(cliaddr);
 
    if((connfd=accept(listenfd,(struct sockaddr*)&cliaddr,&peerlen))<0)
 
    {
 
        perror("accept");
 
        exit(0);
 
    }
 
    printf("Connect:[%s:%d]\n",inet_ntoa(cliaddr.sin_addr),ntohs(cliaddr.sin_port));
 
    if((pthread_create(&sendtid,NULL,thread_send,&connfd))!=0)
 
    {
 
        perror("create thread");
 
        exit(0);
 
    }
 
    while(1)
 
    {
 
        if((recv(connfd,buf,BUFFER,0))<0)
 
        {
 
            perror("recv");
 
            exit(0);
 
        }
 
        printf("Received:%s",buf);
 
        if(strncmp(buf,"byebye",6)==0)
 
        {
 
            break;
 
        }
 
    }
 
    close(connfd);
 
    close(listenfd);
 
    return 0;
 
}
//客户端
 
//文件client_thread.c
 
//注意:由于客户端发送"byebye"后与服务器端断开连接,因此在主进程内发送数据而在线程内接收数据
 
#include<stdio.h>
 
#include<sys/types.h>
 
#include<sys/socket.h>
 
#include<stdlib.h>
 
#include<string.h>
 
#include<unistd.h>
 
#include<netinet/in.h>
 
#include<arpa/inet.h>
 
#include<pthread.h>
 
#define BUFFERSIZE 128
 
void *thread_recv(void *arg)
 
{
 
    int listenfd=*((int*)arg);
 
    char recvbuf[BUFFERSIZE]={0};
 
    while(1)
 
    {
 
        if(recv(listenfd,recvbuf,BUFFERSIZE,0)<0)
 
        {
 
            perror("recv");
 
            exit(0);
 
        }
 
        printf("recv:%s",recvbuf);
 
        bzero(recvbuf,BUFFERSIZE);
 
    }
 
    pthread_exit(NULL);//可以省略
 
}
 
int main()
 
{
 
    int sockfd;
 
    pthread_t pthrecv;
 
    char buf[BUFFERSIZE]={0};
 
    struct sockaddr_in servaddr;
 
    if(argc<3)
 
    {
 
        printf("too few argument\n");
 
        printf("Usage: %s <ip> <port>\n",argv[1]);
 
        exit(0);
 
    }
 
    //创建socket
 
    if((sockfd=socket(AF_INET,SOCK_STREAM,0))<0)
 
    {
 
        perror("socket");
 
        exit(0);
 
    }
 
    //设置sockaddr_in结构体中相关参数
 
    bzero(&servaddr,sizeof(servaddr));
 
    servaddr.sin_family = AF_INET;
 
    servaddr.sin_port = htons(atoi(argv[2]));
 
    servaddr.sin_addr.s_addr = inet_addr(argv[1]);
 
    //调用connect()函数向服务器端建立TCP链接
 
    if(connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
 
    {
 
        perror("connect");
 
        exit(0);
 
    }
 
    //创建线程,用于接收信息
 
    if((pthread_create(&pthrecv,NULL,thread_recv,&sockfd))!=0)
 
    {
 
        perror("创建线程");
 
        exit(0);
 
    }
 
    //发送消息给服务器端
 
    while(1)
 
    {
 
        fgets(buf,BUFFERSIZE,stdin);
 
        send(sockfd,buf,BUFFERSIZE,0);
 
        if(strncmp(buf,"byebye",6)==0)
 
            break;
 
    }
 
    close(sockfd);
 
    return 0;
 
}


UDP编程

只能使用sendto()和recvfrom()!不能使用read()和wirte()

每调用一次sendto()就是发送一个数据报!

每调用一次recvfrom()就是接收一个数据报!

TCP连接中的各种FIN、SYN请求和ack应答都是在传输层实现好了的,TCP协议会直接给应用层提供可靠的连接 但是这会造成额外的开销,因此使用TCP应用层不需要在应用层面进行一些连接可靠性的操作。而UDP在协议层没有实现像TCP那样的可靠性操作,所以可以知道udp会把很多TCP在传输层实现的细节,丢给使用udp传输层协议的udp应用层处理,比如类似于TCP中的拥塞控制,在udp协议层中没有实现,因此需要有udp的应用层的应用程序来进行处理。

为什么udp就不能使用read通过udp的套接字读取数据呢?

UDP协议是基于数据报的,每个数据报都是独立的,没有像TCP协议那样的流式传输。因此,UDP协议中的数据没有固定的消息边界,即一个数据报可能被分成多个数据块发送,或者多个数据报可能被合并成一个数据块接收。这种不确定性使得在UDP协议中使用类似read的函数读取数据非常困难。

另外,UDP协议是无连接的,因此没有像TCP协议那样的已连接状态,也没有类似于TCP协议中的缓冲区和窗口等机制。这意味着,如果像read一样从UDP套接字中读取数据,则无法确定接收到的数据是否是完整的数据报,也无法确定数据报的来源地址和端口。

因此,为了正确地接收UDP协议中的数据,应该使用recvfrom函数来接收数据报,并指定数据报的来源地址和端口。这样可以确保正确地接收到完整的数据报,并且可以根据来源地址和端口进行处理。

1.UDP是基于数据报的协议 所以读取udp的接收缓冲区中的数据应该以数据报为单位?

UDP是基于数据报的协议,每个UDP数据报都是一个完整的数据单元,因此读取UDP接收缓冲区中的数据时,应该以数据报为单位进行读取。也就是说,每次读取时应该读取整个UDP数据报,而不能只读取其中的一部分。在Linux内核中,可以使用recvfrom()函数来读取UDP接收缓冲区中的数据报。该函数会将整个数据报读入缓冲区,并返回数据报的长度和发送方的地址信息。应用程序可以根据需要对数据报进行解析和处理。

2.UDP数据报由什么组成?它的大小如何决定?

UDP数据报由报头和数据两部分组成。报头包含了UDP的源端口号、目的端口号、UDP数据报长度以及校验和等控制信息;数据则是应用程序发送的实际数据。在UDP协议中,每个UDP数据报都是一个完整的数据单元,因此每个UDP数据报的大小是固定的。

UDP数据报的大小由发送端应用程序决定。发送端应用程序将要发送的数据按照UDP数据报的格式组织好后,可以通过调用sendto()函数将数据报发送出去。在发送数据报之前,应用程序需要确定数据报的大小,以便内核分配足够的空间来存储该数据报。在Linux内核中,UDP数据报的最大大小由MTU(最大传输单元)决定,通常为1500字节。如果数据报的大小超过了MTU,则数据报会被分片,每个分片都会成为一个独立的UDP数据报进行传输!

    使用UDP协议通信时服务器端与客户端无需提前建立连接,只需要知道对方的套接字地址信息就可以发送数据。服务器端只需创建一个套接字用于接收不同客户发来的请求,经过处理即可通信。

    由于没有事先建立连接,因此使用UDP协议通信时无法保证数据是否成功接收,因此,若需要保证数据可靠性,则不要使用UDP通信。

使用UDP通信的服务器端的编程流程如下:

创建服务器步骤:

    -创建套接字

    -绑定套接字

    -接收/发送数据

    -断开连接

创建客户端步骤:

 -创建套接字

    -绑定套接字(可选)

    -接收/发送数据

    -断开连接


UDP编程接口函数

UDP编程读写数据只能使用sendto()和recvfrom() 不可以使用read和write!

每调用一次sendto()就是发送一个数据报!

每调用一次recvfrom()就是接收一个数据报!

为什么udp就不能使用read通过udp的套接字读取数据呢?

UDP协议是基于数据报的,每个数据报都是独立的,没有像TCP协议那样的流式传输。因此,UDP协议中的数据没有固定的消息边界,即一个数据报可能被分成多个数据块发送,或者多个数据报可能被合并成一个数据块接收。这种不确定性使得在UDP协议中使用类似read的函数读取数据非常困难。

另外,UDP协议是无连接的,因此没有像TCP协议那样的已连接状态,也没有类似于TCP协议中的缓冲区和窗口等机制。这意味着,如果像read一样从UDP套接字中读取数据,则无法确定接收到的数据是否是完整的数据报,也无法确定数据报的来源地址和端口。

因此,为了正确地接收UDP协议中的数据,应该使用recvfrom函数来接收数据报,并指定数据报的来源地址和端口。这样可以确保正确地接收到完整的数据报,并且可以根据来源地址和端口进行处理。

一些问题:

1.UDP是基于数据报的协议 所以读取udp的接收缓冲区中的数据应该以数据报为单位?

UDP是基于数据报的协议,每个UDP数据报都是一个完整的数据单元,因此读取UDP接收缓冲区中的数据时,应该以数据报为单位进行读取。也就是说,每次读取时应该读取整个UDP数据报,而不能只读取其中的一部分。在Linux内核中,可以使用recvfrom()函数来读取UDP接收缓冲区中的数据报。该函数会将整个数据报读入缓冲区,并返回数据报的长度和发送方的地址信息。应用程序可以根据需要对数据报进行解析和处理。

2.UDP数据报由什么组成?它的大小如何决定?

UDP数据报由报头和数据两部分组成。报头包含了UDP的源端口号、目的端口号、UDP数据报长度以及校验和等控制信息;数据则是应用程序发送的实际数据。在UDP协议中,每个UDP数据报都是一个完整的数据单元,因此每个UDP数据报的大小是固定的。

UDP数据报的大小由发送端应用程序决定。发送端应用程序将要发送的数据按照UDP数据报的格式组织好后,可以通过调用sendto()函数将数据报发送出去。在发送数据报之前,应用程序需要确定数据报的大小,以便内核分配足够的空间来存储该数据报。在Linux内核中,UDP协议中的最大数据长度为65507字节 但是UDP数据报的最大大小由MTU(最大传输单元)(MTU是指在网络中能够传输的最大数据包大小,它包括IP头和数据部分。在IPv4网络中,MTU通常为1500字节,而在IPv6网络中,MTU通常为1280字节。如果UDP数据包的大小超过了MTU,则数据包会被分片,这会增加网络传输的负担和延迟,并且可能会导致数据包丢失或重传。因此,在UDP编程中,我们应该尽量避免发送超过MTU的数据包。如果需要发送超过MTU的数据,我们可以选择将数据分成多个较小的数据包,然后逐个发送。这样可以降低数据包被分片的概率,减少网络传输的负担和延迟,并且提高数据的可靠性。另外,我们也可以使用一些优化技术,如UDP加速、数据压缩等,来提高UDP的性能和可靠性。)决定,通常为1500字节。如果数据报的大小超过了MTU,则数据报会被分片,每个分片都会成为一个独立的UDP数据报进行传输。

sendto()和recvfrom()都适用于TCP和UDP协议的编程!

 1.   函数sendto()

sendto()函数的用法:

    所需头文件:#include<sys/types.h>

                        #include<sys/socket.h>

    函数原型:int sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen)

    函数参数:

        sockfd          需要发送数据的套接字文件描述符

        buf               发送数据的缓冲区首地址

        len               发送的数据的长度

        flags            通常设定为0

        dest_addr    接收方套接字的IP地址与端口号的结构体

        addrlen        dest_addr结构体的大小

    函数返回值:

        成功:实际发送的字节数

        失败:-1

//send(sockfd,buf,len,0)等价于sendto(sockfd,buf,len,0,NULL,0);

  2.  函数recvfrom()

recvfrom()函数的用法:

    所需头文件:#include<sys/types.h>

                        #include<sys/socket.h>

    函数原型:int recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen)

    函数参数:

        sockfd        需要接收数据的套接字文件描述符

        buf            接收数据的缓冲区首地址

        len             接收数据缓冲区能接受的数据长度(如果小于一个数据报就会发生截断)

        flags          通常设定为0

        src_addr    用来保存该数据报发送方套接字的IP地址与端口号的结构体 赋值为NULL就代表不在意数据报发送方的IP地址与端口号.

        addrlen        src_addr结构体的大小

    函数返回值:

        成功:实际接收的字节数

        失败:-1
 

示例:使用UDP进行通信,代码分成服务器端和客户端两部分

//client.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
 #include <arpa/inet.h>

int main(int argc, char*argv[])
{
    int socketfd;
    int datalenth;
    int ret;
    char sendbuf[1024];

    if (argc != 3)
    {
        printf("Usage:./client <ip><port>\n");
        exit(1);
    }

    socketfd = socket (AF_INET, SOCK_DGRAM, 0);
    if (socketfd < 0)
    {
        perror ("socket error!\n");
        exit (-1);
    }


    struct sockaddr_in serveraddr;
    clientaddr.sin_family = AF_INET;
    clientaddr.sin_port = htons (atoi (argv[2]));
    clientaddr.sin_addr.s_addr = inet_addr (argv[1]);

    while (1)
    {
		fgets (sendbuf, 1024, stdin);
		datalenth = strlen (sendbuf) + 1;
		
		ret = sendto (socketfd, sendbuf, datalenth, 0, (const struct sockaddr *)&serveraddr, sizeof(clientaddr));
		if (ret == -1 )
		{
			perror ("sendto error !\n");
			exit (-1);
		}
		else if (ret != datalenth)
		{
			printf ("sendto datalenth error!\n");
		}
		printf ("send data is %s\n", sendbuf);
    }

}
//server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>



int main(int argc, char*argv[])
{

    int serverfd;
    int ret;
    struct sockaddr_in serveraddr;
    char buf[1024];


    printf ("-------------server------------\n\n");

    serverfd = socket (AF_INET, SOCK_DGRAM, 0);
    if (serverfd < 0)
    {
        perror ("serverfd error!\n");
        exit (-1);
    }

    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons (atoi ("6666"));
    serveraddr.sin_addr.s_addr = inet_addr ("192.168.3.10");
    ret = bind (serverfd, (const struct sockaddr *)&serveraddr, sizeof (serveraddr));
    if (ret < 0)
    {
        perror ("bind error!\n");
        exit (-2);
    }

    while (1)
    {
        recvfrom (serverfd, buf, 1024, 0, NULL, NULL);
        printf ("recv data is %s\n", buf);
    }


}

这里使用fgets()读取到的数据作为数据报内容,有个好处就是数据末尾自带 ' \0 '这样在server.c中打印接收到的数据直接使用printf就行了 我们都不用清理buf。

(因为printf ("%s")在读取到 ' \0 '就会停止)


客户端bind()问题:

为什么TCP编程和UDP编程的流程图中:客户端的bind()是可选的也就是非必需的,难道可以不用绑定客户端的地址信息吗?

 在网络编程中,客户端通常不需要调用 bind 函数来显式绑定一个特定的 IP 地址和端口。通常情况下,客户端会自动分配一个临时的端口号,并将其与服务端的 IP 地址和端口建立连接。

客户端在与服务端建立连接时,通常会调用 socket 创建一个套接字,然后使用 connect 函数与服务端建立连接。在 connect 函数中,客户端需要指定服务端的 IP 地址和端口。示例代码如下:

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
    perror("socket");
    exit(EXIT_FAILURE);
}

struct sockaddr_in serverAddr;
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(PORT);
serverAddr.sin_addr.s_addr = inet_addr(SERVER_IP);

if (connect(sockfd, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) < 0) {
    perror("connect");
    exit(EXIT_FAILURE);
}

在上述代码中,客户端通过 connect 函数与服务端建立连接。在 connect 函数中,客户端传入服务端的 IP 地址和端口信息,以及套接字描述符。客户端的 IP 地址和端口将由操作系统自动分配和绑定。

需要注意的是,虽然客户端通常不需要显式调用 bind 函数,但在某些特定场景下,客户端也可以通过调用 bind 函数绑定一个特定的 IP 地址和端口。这通常用于需要客户端具有特定源 IP 地址或特定的本地端口的情况,如特定网络配置或连接池的需求等。但在一般情况下,客户端并不需要进行显式的 bind 操作。


服务端如何判断客户端是否主动或者异常断开连接了?

在服务端判断客户端是否断开连接的方式,常见的有以下几种方法:

1.读取数据返回值和错误码:在服务端从客户端读取数据时,可以通过读取函数的返回值来判断客户端是否断开连接。例如,使用 read 函数从套接字读取数据时,如果返回值为小于零,则表示客户端已关闭连接。

 n = read(connsockfd, readbuf, sizeof(readbuf), 0));
if((n < 0) //出现异常read会返回-1
{  
    if (errno == ECONNRESET)//客户端异常关闭连接 错误码会被置为ECONNRESET
    {  
        close(connsockfd);  
    }
    else 
        fprintf(stderr, "recv function failed.\n");
    } 
else if(n == 0)//read套接字描述符返回0说明对端关闭
{
    close(connsockfd);  
}

2.设置套接字超时:可以通过设置套接字的超时选项,来判断客户端是否断开连接。通过设置适当的超时时间,如果在规定时间内没有接收到客户端的数据,可以认为客户端已断开连接。

struct timeval timeout;
timeout.tv_sec = 10; // 设置超时时间为 10 秒
timeout.tv_usec = 0;
setsockopt(clientSocket, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout, sizeof(timeout));

在后续的读取操作中,如果超过指定的超时时间没有接收到数据,可以将客户端视为已断开连接。

3.使用心跳机制:在客户端和服务端之间使用心跳机制,定期发送心跳消息来维持连接。服务端可以通过检测心跳消息的接收情况来判断客户端是否断开连接。如果一定时间内没有接收到心跳消息,可以认为客户端已断开连接。

4.使用信号和异步事件处理:在某些情况下,可以使用信号(如 SIGPIPE)或异步事件处理机制来检测客户端断开连接的事件。通过捕获相应的信号或处理相应的事件回调,可以得知客户端的连接状态变化。


Socket TCP连接中的TIME-WAIT

preview

一个socket连接,可以得出以下几点:

1) 主动关闭连接的一方 – 也就是主动调用socket的close操作的一方,最终会进入TIME_WAIT状态 ;
2) 被动关闭连接的一方,有一个中间状态,即CLOSE_WAIT,因为协议层在等待上层的应用程序,主动调用close操作后才主动关闭这条连接 ;(其实被动关闭连接的一方的CLOSE_WAIT就是在等待上层的应用程序传输数据结束)
3) TIME_WAIT会默认等待2MSL时间后,才最终进入CLOSED状态;
4) 在一个连接没有进入CLOSED状态之前,这个连接是不能被重用的

time_wait的作用,以及为什么是2*MSL
1. 防止连接关闭时四次挥手中的最后一次ACK丢失

        如果没有TIME_WAIT状态,主动关闭一方(客户端)就会在收到对端(服务器)的FIN并回复ACK后 直接从FIN_WAIT_2 进入 CLOSED状态,并释放连接。此时如果最后一条ACK丢失,那么服务器重传的FIN将无人处理,最后导致服务器长时间的处于 LAST_ACK状态而无法正常关闭(服务器只能等到达到FIN的最大重传次数后关闭)

        如果此时新建一个连接,源随机端口如果被复用,在connect发送SYN包后,由于被动方仍认为这条连接【五元组】还在等待ACK,但是却收到了SYN,则被动方会回复RST; 造成主动创建连接的一方,由于收到了RST,则连接无法成功;

        将TIME_WAIT的时长设置为 2MSL,是因为报文在链路中的最大生存时间为MSL(Maximum Segment Lifetime),超过这个时长后报文就会被丢弃。TIME_WAIT的时长则是:最后一次ACK传输到服务器的时间 + 服务器重传FIN 的时间,即为 2MSL。

2. 防止新连接收到旧链接的TCP报文:

TCP使用四元组区分一个连接(源端口、目的端口、源IP、目的IP),如果新、旧连接的IP与端口号完全一致,则内核协议栈无法区分这两条连接。在关闭“前一个连接”之后,马上又重新建立起一个相同的IP和端口之间的“新连接”,“前一个连接”的迷途重复分组在“前一个连接”终止后到达,而被“新连接”收到了。2*MSL 的时间足以保证两个方向上的数据都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定是新连接上产生的。
 

在 TCP 中,当一个连接被正常关闭或中断后,操作系统会将该连接的套接字保持在 TIME-WAIT 状态一段时间。这是为了确保网络中的所有数据报都能够正常传递,并防止新的连接与旧连接之间的数据混淆。在 TIME-WAIT 状态期间,同样的 IP 地址和端口号无法立即被新的连接使用。

而在某些情况下,当一个服务器程序重新启动后,它可能需要在之前使用的 IP 地址和端口上重新绑定并接受新的连接,而不受 TIME-WAIT 状态的影响。这时可以通过setsockopt使用 SO_REUSEADDR 选项来实现地址复用,允许新的套接字重新绑定到相同的 IP 地址和端口上。


 setsockopt函数

setsockopt函数_坏坏太兲眞的博客-CSDN博客初学者必学的四种设置setsockopt函数方式。第一种用于广播设置、第二种用于多播设置、第三种增加地址复用功能、第四种增加端口复用功能。https://blog.csdn.net/weixin_47783699/article/details/128038952

setsockopt函数的作用:

用于任意类型、任意状态套接字的设置选项值。尽管在不同协议层上存在选项,但本函数仅定义了最高的“套接口”层次上的选项。

    其实意思就是对原本套接字的功能增强,如果你只调用socket()函数,只是有一般的功能,你去使用setsockopt函数有另外的一些功能,比如设置接收缓冲区的大小、禁用一些协议的功能等等等等

#include <sys/socket.h>
 
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
 
参数说明:
(1) int sockfd: 很简单,socket描述符
(2) int level: 选项定义的层次;SOL_SOCKET      套接字层次
                                                 IPPROTO_TCP    TCP层次
                                                 IPPROTO_IP        IP层次

                                                 IPPROTO_UDP     UDP层次
(3) int optname: 需设置的选项
(4) const void *optval: 指针,指向存放选项值的缓冲区

                                 选项值 第一种设置:  int类型的值

                                                                         0         不允许
                                                                         非0值  允许

PS:参数的值解引用的值为0表示设置的选项不允许使用;为非0值表示允许使用。通过这样可以实现启用某些功能或者禁用某些功能。

例如: 第二第三个参数设置为SOL_SOCKET和SO_BROADCAST  第四个参数解引用的值 如果是0则为禁用广播,如果是1则为使用广播。这就是第四个参数解引用的值设置为0或者1的作用

                                                 第二种设置: ip_mreq{}类型  

                                                          struct ip_mreq
                                                         {
                                                           struct in_addr imr_multiaddr; //组播ip地址
                                                           struct in_addr imr_interface; //主机地址 常赋为 INADDR_ANY 任意主机地址(自动获取你的主机地址)
                                                            };
                                                   

(5) socklen_t optlen: optval缓冲区的长度

返回值:成功返回0 失败返回-1

第2、3、4参数常用的如下:(下表的最后一列为第四个参数的类型)

  1. SO_REUSEADDR允许地址重用,主要针对TCP连接,因为UDP没有TIME-WAIT这个状态。可以在关闭服务器后立即重新启动而无需等待一段时间。对于服务器程序,如果你希望在关闭后立即重新启动,可以设置该选项。在编写服务器端的代码时一般都要设置这个选项,这个选项和主动结束连接或者异常结束连接的TIME-WAIT状态有关。举个例子:1.服务器启动后,与客户端建立连接,如果服务器主动关闭,那么和客户端的连接会处于TIME_WAIT状态,此将无法启动服务器进程。2.服务器父进程监听客户端,建立连接后,fork一个子进程专门处理客户端的请求,如果父进程停止,因为子进程还和客户端有连接,所以此时重启父进程会失败。
    对于以上两张情况,重启服务器都会出现bind: : Address already in use错误。而当我们使用了 SO_REUSEADDR 选项后,服务器退出后,仍然允许我们马上重启进程。可以看看这个博客:Socket中SO_REUSEADDR详解_明潮的博客-CSDN博客

  2. SO_REUSEPORT: 允许端口重用。如果被捆绑的IP地址是一个多播地址,则SO_REUSEADDR和SO_REUSEPORT等效。

    使用这两个套接口选项的建议:

     a.   在所有TCP服务器中,在调用bind之前设置SO_REUSEADDR套接口选项;

    b.    当编写一个同一时刻在同一主机上可运行多次的多播应用程序时,设置SO_REUSEADDR选项,并将本组的多播地址作为本地IP地址捆绑。

  3. SO_BROADCAST:允许发送广播数据报

  4. SO_KEEPALIVE:启用套接字的心跳检测机制。当启用后,套接字会自动发送心跳信号以检测连接的状态。这对于检测空闲或断开的连接很有用。

  5. SO_RCVBUFSO_SNDBUF:设置接收缓冲区和发送缓冲区的大小。可以使用这些选项来优化套接字的性能,根据实际需求调整缓冲区的大小。

  6. TCP_NODELAY:禁用 Nagle 算法,允许小数据包的立即发送。在某些情况下,这可以提高数据传输的实时性和响应性。(可以防止TCP传输的发送方粘包)

  7. IP_TTL:设置 IP 数据包的生存时间(Time-To-Live)。可以使用这个选项来控制数据包在网络中的跳数限制。

上面这些比较常用


 

如何理解setsockopt()第二个参数的套接字层次(SOL_SOCKET)、TCP层次(IPPROTO_TCP )、UDP层次(IPPROTO_UDP)、IP层次(IPPROTO_IP)?
其实意思就是调用setsockopt是要设置什么,是要设置套接字?还是要设置TCP或者UDP或者IP这些协议上的东西。
1. 比如第二个参数设置为SOL_SOCKET 就是要对套接字进行设置 例如设置成非阻塞的套接字让这个套接字在accept的时候不用阻塞直接返回、例如设置成SO_REUSEADDR 让这个套接字对应的连接避免出现TIME-WAIT的问题、又例如可以设置SO_BROADCAST允许套接字可以进行广播等。
例如:
int optval = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
2. 而设置成IPPROTO_TCP、IPPROTO_UDP、IPPROTO_IP 通常是用来设置协议上的东西 比如设置发送缓冲区的大小、接受缓冲区的大小等等,例如设置成IPPROTO_TCP下使用TCP_NODELAY禁用Nagle算法避免发送方粘包问题:
int optval = 1;
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &optval, sizeof(optval));


UDP的单播、广播和组播

广播、组播和单播

广播、组播和单播是网络通信中常用的三种数据传输方式:

  1. 单播(Unicast):单播是指一对一的数据传输方式,即数据从一个发送者发送到一个接收者。在单播中,发送者和接收者之间需要建立一条独立的通信链路,数据只能被一个接收者接收。

  2. 广播(Broadcast):广播是指一对多的数据传输方式,即数据从一个发送者发送到所有的接收者。在广播中,发送者不需要知道接收者的地址,数据会被传输到所有的网络节点中,所有节点都会接收到这份数据。
    值得注意的是:使用广播传输数据 指定的数据接收方的IP地址 必须是xxx.xxx.xxx.255
                               指定的数据接收方MAC地址必须是全为1 也就是ff:ff: ff: ff:ff:ff
    广播传输的数据可以通过网络节点中的所有主机的数据链路层和网络层(原因在于指定的IP地址和MAC地址);但是无法通过所有的传输层 这是因为进行数据传输需要指定MAC地址、IP地址、端口号。所以虽然通过广播可以实现让当前网络节点中的所有主机在数据链路层和网络层接收到数据,但是无法让所有的主机都在传输层接收到数据,只有主机中存在一个端口,和数据发送方在传输时指定的端口号相同 才能在传输层接收到数据。
    举个例子:主机1 mac地址:xx:xx:xx:xx:xx:xx IP地址:191.168.1.xxx 端口号2
                      主机2 mac地址:xx:xx:xx:xx:xx:xx IP地址:191.168.1.xxx 端口号3
                      主机3 mac地址:xx:xx:xx:xx:xx:xx IP地址:191.168.1.xxx 端口号200
    此时数据发送方指定的 mac地址:ff:ff: ff: ff:ff:ff IP地址:191.168.1.255 端口号200
    就代表进行了广播 数据到达了主机1、2、3的数据链路层和网络层,但是由于指定的端口号为200,所以只有主机3在传输层接收到了数据

  3. 组播(Multicast):组播是指一对多的数据传输方式,但是相比于广播,组播只会将数据传输到指定的一组接收者。在组播中,发送者需要知道接收者的组地址,数据只会被传输到这个组中的所有接收者。

总之,单播、广播和组播是网络通信中常用的三种数据传输方式,每种方式都有其优缺点,根据不同的需求选择不同的方式可以提高网络通信的效率和可靠性。

 传输层协议中只有UDP可以广播和组播


UDP编程实现广播

其实只要注意两点:

1.在调用socket()后 再调用 setsockopt()来设置广播选项

2.数据接收方的IP地址,只要是xxx.xxx.xxx.255就行了

具体步骤如下:

1.创建UDP套接字

int sockfd = socket(AF_INET, SOCK_DGRAM, 0);

2.设置广播选项

int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &opt, sizeof(opt));

3.绑定本地IP地址和端口号

struct sockaddr_in local_addr;
bzero(&local_addr, sizeof(local_addr));
local_addr.sin_family = AF_INET;
local_addr.sin_port = htons(LOCAL_PORT);
local_addr.sin_addr.s_addr = INADDR_ANY;
bind(sockfd, (struct sockaddr*)&local_addr, sizeof(local_addr));

4.设置目标IP地址和端口号

struct sockaddr_in target_addr;
bzero(&target_addr, sizeof(target_addr));
target_addr.sin_family = AF_INET;
target_addr.sin_port = htons(TARGET_PORT);
target_addr.sin_addr.s_addr = inet_addr(TARGET_IP);

5.发送广播消息

const char* msg = "Hello, broadcast!";
sendto(sockfd, msg, strlen(msg), 0, (struct sockaddr*)&target_addr, sizeof(target_addr));

6.接收广播消息

struct sockaddr_in from_addr;
bzero(&from_addr, sizeof(from_addr));
char buf[MAX_BUF_SIZE];
socklen_t from_len = sizeof(from_addr);
recvfrom(sockfd, buf, MAX_BUF_SIZE, 0, (struct sockaddr*)&from_addr, &from_len);
printf("Received broadcast message from %s:%d: %s\n", inet_ntoa(from_addr.sin_addr),
       ntohs(from_addr.sin_port), buf);

完整代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define LOCAL_PORT 8888
#define TARGET_PORT 9999
#define TARGET_IP "255.255.255.255"
#define MAX_BUF_SIZE 1024

int main()
{
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket");
        exit(-1);
    }

    int opt = 1;
    setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &opt, sizeof(opt));

    struct sockaddr_in local_addr;
    bzero(&local_addr, sizeof(local_addr));
    local_addr.sin_family = AF_INET;
    local_addr.sin_port = htons(LOCAL_PORT);
    local_addr.sin_addr.s_addr = INADDR_ANY;
    if (bind(sockfd, (struct sockaddr*)&local_addr, sizeof(local_addr)) < 0) {
        perror("bind");
        exit(-1);
    }

    struct sockaddr_in target_addr;
    bzero(&target_addr, sizeof(target_addr));
    target_addr.sin_family = AF_INET;
    target_addr.sin_port = htons(TARGET_PORT);
    target_addr.sin_addr.s_addr = inet_addr(TARGET_IP);

    const char* msg = "Hello, broadcast!";
    if (sendto(sockfd, msg, strlen(msg), 0, (struct sockaddr*)&target_addr, sizeof(target_addr)) < 0) {
        perror("sendto");
        exit(-1);
    }

    struct sockaddr_in from_addr;
    bzero(&from_addr, sizeof(from_addr));
    char buf[MAX_BUF_SIZE];
    socklen_t from_len = sizeof(from_addr);
    if (recvfrom(sockfd, buf, MAX_BUF_SIZE, 0, (struct sockaddr*)&from_addr, &from_len) < 0) {
        perror("recvfrom");
        exit(-1);
    }
    printf("Received broadcast message from %s:%d: %s\n", inet_ntoa(from_addr.sin_addr),
           ntohs(from_addr.sin_port), buf);

    close(sockfd);

    return 0;
}


UDP实现组播(多播)

 UDP组播发送端

1、建立套接字()

2、发送数据,往群聊(组播地址)中发送数据

3、关闭

UDP组播接收端

1、建立套接字

2.定义组播结构体

3、设置组播ip(初始化 组播结构体)

4.加入组播属性(也就是设置这个套接字 可以接收组播信息)

5、绑定IP地址和端口号

6、进群,加入群聊,将当前的IP地址设置到组播地址中

7、创建结构体存放客户端IP和端口,接收数据

8、关闭
 

 UDP实现组播:

1.c语言实现udp广播和组播_udp组播接收和发送程序_sakura0908的博客-CSDN博客

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
 
#define GROUP_IP    "224.0.0.10"  //组播地址224开头
#define GROUP_PORT  60000
 
#define MY_IP   "192.168.5.184"
 
//udp组播server.c
int main()
{
    int ret = 0;
 
    //1.建立套接字
    int socket_fd = socket(AF_INET,SOCK_DGRAM,0);
    if(socket_fd < 0)
    {
        perror("socket fail");
        return -1;
    }
 
    //2.定义组播结构体
    struct ip_mreq vmreq;
 
    //3、设置组播ip(初始化 组播结构体)
    inet_pton(AF_INET,"224.0.0.10",&vmreq.imr_multiaddr); // 组播地址
    inet_pton(AF_INET,"192.168.5.184",&vmreq.imr_interface); // 需要添加到组的ip
 
    //4.加入组播属性(也就是设置这个套接字 可以接收组播信息)
    setsockopt(socket_fd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&vmreq,sizeof(vmreq));
 
    //5.绑定本机地址
    struct sockaddr_in my_addr;
    my_addr.sin_family = AF_INET;//地址族
    my_addr.sin_port = htons(GROUP_PORT);//端口号
    my_addr.sin_addr.s_addr = htonl(INADDR_ANY); //INADDR_ANY代表本机所有地址 常用方法 注意
    ret = bind(socket_fd,(struct sockaddr *)&my_addr,sizeof(struct sockaddr_in));
    if(ret < 0)
    {
        perror("bind fail");
        return -1;
    }
    printf("绑定本机成功[%s][%d]\n",GROUP_IP,GROUP_PORT);
 
    //6.接收数据
    char buf[1024] = {0};
    struct sockaddr_in recv_addr;
    socklen_t addrlen = sizeof(struct sockaddr_in);
    char *ip = NULL;
    int port = 0;
    while(1)
    {
        bzero(buf,sizeof(buf));
 
        ret = recvfrom(socket_fd,buf,sizeof(buf),0,(struct sockaddr *)&recv_addr,&addrlen);
        
        ip = inet_ntoa(recv_addr.sin_addr);
        port = ntohs(recv_addr.sin_port);
 
        printf("[%s][%d]buf:%s ret:%d\n",ip,port,buf,ret);
    }
 
    //关闭套接字
    close(socket_fd);
 
    return 0;
}
 
//udp组播client.c
int main()
{
    int ret = 0;
    char buf[1024] = { 0 };
 
    //1、建立套接字
    int socket_fd = socket(AF_INET,SOCK_DGRAM,0);
    if(socket_fd < 0)
    {
        perror("socket fail");
        return -1;
    }
 
    //给组播地址发送数据
    struct sockaddr_in send_addr;
    send_addr.sin_family = AF_INET;//地址族
    send_addr.sin_port = htons(GROUP_PORT);//端口号
    send_addr.sin_addr.s_addr = inet_addr(GROUP_IP); //ip地址
 
    while(1)
    {
        //清空缓存区
        bzero(buf,sizeof(buf));
        
        scanf("%s",buf);
 
        ret = sendto(socket_fd,buf,strlen(buf),0,(struct sockaddr *)&send_addr,sizeof(struct sockaddr_in ));
        
        printf("发送数据 ret:%d\n",ret);
 
    }
 
    //关闭套接字
    close(socket_fd);
 
    return 0;
}

2.https://blog.csdn.net/Liangren_/article/details/116029676

#include <stdio.h>
#include <sys/socket.h>
 
#define __PORT_SERVER 8101                                              /* 服务器端口号                 */
#define GROUP_ADDR_01 "224.100.200.1"                                   /* 组播端口号 1                 */
#define GROUP_ADDR_52 "224.100.200.52"                                  /* 组播端口号 2                 */
 
 
int main (int argc, char **argv)
{
    int iRet = -1;                                                      /* 操作结果返回值               */
    int sockFd = -1;                                                    /* socket 描述符                */
    struct sockaddr_in sockaddrinRemote;                                /* 远端地址                     */
    struct sockaddr_in sockaddrinLocal;
    struct ip_mreq mreq;
    socklen_t uiAddrLen = sizeof(struct sockaddr_in);
    char cRecvBuff[257] ={0};
    register ssize_t sstRecv = 0;                                       /* 接收到的数据长度             */
 
    /*
     * 创建 socket 连接
     */
    sockFd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    if (sockFd < 0) {
        fprintf(stderr, "create socket error.\n");
        return (-1);
    }
 
    /*
     * 增加地址复用
     */
    iRet = setsockopt(sockFd, SOL_SOCKET, SO_REUSEADDR, &iRet, sizeof(iRet));
    if (iRet < 0) {
        fprintf(stderr, "setsockopt SO_REUSEADDR error.\n");
        return (-1);
    }
 
    /*
     * 初始化本地地址信息
     */
    memset(&sockaddrinLocal, 0, sizeof(sockaddrinRemote));              /* 清空地址信息                 */
    sockaddrinLocal.sin_len = sizeof(struct sockaddr_in);               /* 地址结构大小                 */
    sockaddrinLocal.sin_family = AF_INET;                               /* 地址族                       */
    sockaddrinLocal.sin_port = htons(__PORT_SERVER);                    /* 绑定服务器端口               */
    sockaddrinLocal.sin_addr.s_addr  = INADDR_ANY;
 
    /*
     * 绑定本地地址与端口
     */
    iRet = bind(sockFd, (struct sockaddr *)&sockaddrinLocal, sizeof(sockaddrinLocal));
    if (iRet < 0) {                                                     /* 绑定操作失败                 */
        close(sockFd);                                                  /* 关闭已经创建的 socket        */
        fprintf(stderr, "UDP echo server bind error.\n");
        return (-1);                                                    /* 错误返回                     */
    }
 
    /*
     * 多播结构体初始化
     */
    mreq.imr_multiaddr.s_addr = inet_addr(GROUP_ADDR_01);               /* 多播组的IP地址               */
    mreq.imr_interface.s_addr = htonl(INADDR_ANY);                      /* 加入的客服端主机IP地址       */
    if (setsockopt(sockFd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) == -1)    {
        perror("setsockopt");
        exit(-1);
    }
 
    mreq.imr_multiaddr.s_addr = inet_addr(GROUP_ADDR_52);               /* 多播组的IP地址               */
    if (setsockopt(sockFd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) == -1)    {
        perror("setsockopt");
        exit(-1);
    }
 
    while(1)
    {
        memset(cRecvBuff, 0, sizeof(cRecvBuff));
        /*
         * 从远端接收数据
         */
        sstRecv = recvfrom(sockFd, (void *)&cRecvBuff[0], 257, 0, (struct sockaddr *)&sockaddrinRemote,&uiAddrLen);
        if (sstRecv <= 0) {                                             /* 接收数据失败                 */
            if ((errno != ETIMEDOUT ) && (errno != EWOULDBLOCK)) {      /* 非超时与非阻塞               */
                close(sockFd);                                          /* 关闭已经创建的 socket        */
                fprintf(stderr, "UDP echo server recvfrom error.\n");
                return (-1);
            }
            continue;
        }
 
        sendto(sockFd, (const void *)&cRecvBuff[0], sstRecv, 0, (const struct sockaddr *)&sockaddrinRemote, uiAddrLen);
    }
 
    /*
     * 多播关闭
     */
    setsockopt(sockFd, IPPROTO_IP, IP_DROP_MEMBERSHIP,&mreq, sizeof(mreq));
 
    return  (0);
}


原始套接字

原始套接字(各种协议的分析)_看见代码就想敲的博客-CSDN博客


IO模型

IO执行的两个阶段


  在Linux中,对于一次读取IO的操作,数据并不会被直接拷贝到程序的程序缓冲区。通常包括两个不同阶段:
 (1)等待数据准备好,到达内核缓冲区;
 (2)从内核向进程复制数据。
  对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所有等待分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用程序缓冲区。


1、IO模型与分类分类


    IO,即input&output,指的是计算机与用户之间进行的数据交互方式。在Linux系统中,不同情况下适用的IO场景也不尽相同。

Linux系统将IO分成5种IO模型,分别是:

    -阻塞型IO(blocking IO)

        最常见的IO模型,大多数的程序的默认IO模型都是阻塞型IO。例如之前我们熟悉的scanf()、read()、write()、recv()、send()、accept()、connect()等函数都使用该IO模型。

    -非阻塞型IO(Non-blocking IO)

        并不常见的IO类型。采用非阻塞型IO的程序会在IO失败时立即返回错误值。

    -IO多路复用(IO Multiplexing)

        当程序中同时处理多路IO时采用。

    -信号驱动型IO(signal driven IO)

        非常罕见,这里不讨论。

    -异步IO(asynchronous IO)

        从Linux内核2.6版本后才引入的IO模型,不太常见。简单说是read()端与write()端可以不同时运行,当读取数据端调用read()时可立即进行其余操作,等待内核返回“数据读取完毕”信号后再将数据读取。

//本文中重点讨论前三种IO模型,即阻塞型IO、非阻塞型IO和IO多路复用。

2、阻塞(blocking)


在IO模型中,我们提到了“阻塞”这个概念。什么是阻塞呢?

    阻塞(blocking)指的是在函数得到调用结果返回之前,由于种种原因进程并未得到操作系统的立即响应,进程暂时被挂起,等待相应事件出现后才会被唤醒。

例如在我们熟悉的scanf()函数中,当我们在代码中使用scanf()读取数据时,若暂时未输入数据,此时进程会一直被挂起直至用户输入相应数据为止。在数据被送入stdin之前,整个进程处在阻塞状态,直至用户输入完成才会继续运行。

    缺点:浪费大量系统资源

3、阻塞型IO


//阻塞型IO模型

 阻塞型IO两个阶段都是阻塞的

阻塞型IO是Linux系统内最普遍的IO模式,大多数程序在默认情况下都使用阻塞型IO进行输入输出。之前学习过的大多数读/写函数都是阻塞型IO,例如:

    -读操作:read()、recv()、recvfrom()等

    -写操作:write()、send()等

    -其他操作:accept()、connect()等

具体还可以细分成 读阻塞 和 写阻塞 两种:

    1.读阻塞(以recvfrom()为例)
    当用户进程调用了一个recvfrom()函数时,此时进程从用户态切换至内核态,kernel开始IO第一阶段:准备数据。在有足够的数据到来之前,内核不会进行进一步动作直至数据全部接收。当所有数据接收完毕,kernel开始IO第二阶段:数据拷贝。kernel将收到的数据全部从内核空间拷贝到用户空间中,然后kernel返回结果。在阻塞型IO的两个阶段(等待数据阶段和拷贝数据阶段),整个用户进程都被阻塞,即从“调用recvfrom()函数”开始,直至“收到内核返回结果”,整段时间内用户进程都处在被阻塞的状态中。

    2.写阻塞
    写阻塞的发生状况比读阻塞少得多。主要发生在当写入的缓冲区空间不足时,后续数据不会立即写入缓冲区,等待缓冲区内重新有写入空间。在等待缓冲区有空间这段时间内进程会出现写阻塞的情况。
 

4、非阻塞型IO


//非阻塞型IO模型

非阻塞IO在第一阶段是不会阻塞的,但是在第二阶段还是会阻塞 

当我们使用非阻塞型IO进行数据读写时,内核会对本次IO是否能够立即获得数据进行判断:

    -若可以立即得到结果,则直接操作,并返回成功

    -若无法立即得到结果,则会立即返回错误而不会阻塞进程等待

    使用非阻塞型IO时,本次IO是否成功会立即得到结果而不会像阻塞型IO一样一直挂起等待。当然,由于无法得知何时能够得到数据,所以需要一个循环来不停测试数据是否已经可读。该过程称为"polling"(原意为“民意选举”,这里指“调查”)。

    使用非阻塞型IO时,进程需要不断poling内核检查IO操作是否已完成,这是一个十分浪费CPU的操作,因此非阻塞型IO基本不采用。
 

5、IO多路复用

  IO 多路复用的好处就在于单个进程就可以同时处理多个网络连接的IO。它的基本原理就是不再由应用程序自己监视连接,取而代之由内核替应用程序监视文件描述符。
  以select为例,当用户进程调用了select,那么整个进程会被阻塞,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从内核拷贝到用户进程。如图:
 

I/O多路复用实际上就是用select , poll, epoll监听多个io对象,当io对象有变化(有数据)的时候就通知用户进程。好处就是单个进程可以处理多个socket。当然具体区别我们后面再讨论,现在先来看下I/O多路复用的流程:

(1)当用户进程调用了select,那么整个进程会被block;

​ (2)而同时,kernel会“监视”所有select负责的socket;

(3)当任何一个socket中的数据准备好了,select就会返回;

(4)这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。

所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。

这个图和blocking IO的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。

所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用多线程 + 阻塞 IO的web server性能更好,可能延迟还更大。

select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。

多路复用函数
在Linux系统中,常用的实现IO复用的函数有三个:select()、poll()和epoll()函数族

1.select()函数
select()函数是最早(1983年在BSD内)实现IO多路复用的函数。select()的用法如下:

    函数select()

    所需头文件:#include<sys/types.h>

                        #include<sys/time.h>

                        #include<unistd.h>

    函数原型:int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)

    函数参数:

        nfds              所有监控的文件描述符中的最大值加1(注意必须加1否则可能出错)

        readfds         需要监控的所有读的文件描述符

        writefds        需要监控的所有写的文件描述符

        exceptfds      需要监控的所有额外操作的文件描述符(例如错误检查等)

        //readfds、writefds、exceptfds三个参数需要使用特定的函数设定,见下

        //三个参数可以省略,若省略则设置为NULL

        timeout        超时时间。设定select()的等待时长。其中必须设定struct timeval类型结构体变量并使用地址传递,该结构体如下:

            //该结构体在头文件<sys/time.h>中

struct timeval

            {undefined

                long tv_sec;    //秒

                long tv_usec;    //毫秒

            };

        若该参数设定为NULL,则表示一直阻塞直至有文件描述符准备就绪。若不为NULL,则会在指定时长内等待事件发生直至超时返回。

    函数返回值:

        成功:返回处于就绪态并且包含在fd_set结构中的描述符总数

        失败:

            0    超时

            -1    错误

    设定readfds、writefds、exceptfds三个参数需要使用以下几个函数设定:

    void FD_SET(int fd, fd_set *fdset)     //将fd加入fdset

    void FD_CLR(int fd, fd_set *fdset)    //将fd从fdset中剔除

    void FD_ZERO(fdset *fdset)             //清除fdset中所有文件描述符

    int FD_ISSET(int fd, fd_set *fdset)    //判断fd是否在fdset中。在调用select()之后使用

调用select()之后,进程会一直等待直至出现以下某种情况:

    -有文件可读

    -有文件可写

    -超时
 

示例1:使用select()监控stdin,若stdin在5s内无数据,则打印提示信息;若有数据,则输出数据

#include <stdio.h>
 
#include <stdlib.h>
 
#include <sys/time.h>
 
#include <sys/types.h>
 
#include <unistd.h>
 
#define MAXSIZE 128
 
int main()
 
{
 
    fd_set rfds;//监控的文件描述符集合
 
    struct timeval tv;//设定超时时间
 
    int retval;
 
    char readbuffer[MAXSIZE];
 
 
 
    FD_ZERO(&rfds);//使用之前先清空
 
    FD_SET(0, &rfds);//将stdin(文件描述符为0)加入rfds
 
 
 
    tv.tv_sec = 5;//设定时间5秒
 
    tv.tv_usec = 0;
 
 
 
    retval = select(1, &rfds, NULL, NULL, &tv);//注意第一个参数为0+1=1
 
    if (retval == -1)
 
        perror("select()");
 
    else if (retval)//监控到某个文件描述符就绪
 
    {
 
        printf("Data is available now.\n");
 
        if(FD_ISSET(0, &rfds)!=0)//需要使用FD_ISSET()查看是哪个文件描述符就绪
 
        {
 
            fgets(readbuffer,MAXSIZE,stdin);
 
            printf("string:%s",readbuffer);
 
        }
 
    }
 
    else//超时
 
        printf("No data within five seconds.\n");
 
 
 
    return 0;
 
}

示例2:在TCP服务器端使用select(),监控stdin与客户端连接。若stdin中有数据则将stdin内数据输出,若有客户端连接则向客户端发送信息

#include <stdio.h>
 
#include <stdlib.h>
 
#include <unistd.h>
 
#include <string.h>
 
#include <sys/types.h>
 
#include <sys/select.h>
 
#include <sys/socket.h>
 
#include <arpa/inet.h>
 
#define N 64
 
 
 
int main(int argc, const char *argv[])
 
{
 
    int i, listenfd, connfd, maxfd;
 
    char buf[N];
 
    fd_set rdfs;
 
    struct sockaddr_in myaddr;
 
    if(argc<3)
 
    {
 
        printf("too few argument\n");
 
        printf("Usage: %s <ip> <port>\n",argv[1]);
 
        exit(0);
 
    }
 
    if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
 
    {
 
        perror("fail to socket");
 
        exit(-1);
 
    }
 
 
 
    bzero(&myaddr, sizeof(myaddr));
 
    myaddr.sin_family = AF_INET;
 
    myaddr.sin_port = htons(atoi(argv[2]));
 
    myaddr.sin_addr.s_addr = inet_addr(argv[1]);
 
 
 
    if (bind(listenfd, (struct sockaddr *)&myaddr, sizeof(myaddr)) < 0)
 
    {
 
        perror("fail to bind");
 
        exit(-1);
 
    }
 
    listen(listenfd, 5);
 
    maxfd = listenfd;//由于stdin的文件描述符为0,所以maxfd是listenfd
 
 
 
    while(1)
 
    {
 
        FD_ZERO(&rdfs);//使用之前先清空rdfs
 
        FD_SET(0, &rdfs);//将stdin加入rdfs
 
        FD_SET(listenfd, &rdfs);//将listenfd加入rdfs
 
        //select()会修改传入的值,因此每次都需要重新设置这三个函数
 
        if (select(maxfd+1, &rdfs, NULL, NULL, NULL) < 0)//超时时间设定为NULL
 
        {
 
            perror("fail to select");
 
            exit(-1);
 
        }
 
 
 
        for(i=0; i<=maxfd; i++)
 
        {
 
            if (FD_ISSET(i, &rdfs))//监控到数据,查看数据来源
 
            {
 
                if (i == STDIN_FILENO)//来自stdin
 
                {
 
                    printf("STDIN is coming\n");
 
                    fgets(buf, N, stdin);
 
                    printf("string:%s", buf);
 
                }
 
                else if (i == listenfd)//来自客户端
 
                {
 
                    connfd = accept(i, NULL, NULL);
 
                    printf("New Connection connfd=%d is coming\n", connfd);
 
                    strcpy(buf,"Hello, client");
 
                    send(connfd,buf,N,0);
 
                    bzero(buf, N);
 
                    close(connfd);
 
                }
 
            }
 
        }
 
    }
 
    return 0;
 
}   

2.poll()函数与epoll()函数


select()是最早实现IO多路复用的函数,但是它暴露出了许多问题:

    -select()有可能修改传入参数,这样对于一个需要多次调用的函数是非常不友好的(每次都需要重新设定)

    -select()仅仅会返回,但不会告知哪个文件描述符得到了数据,因此需要FD_ISSET()筛选。如果文件描述符过多则十分浪费时间

    -select()最多只能监控1024个文件描述符(且无法更改)

    -select()不是线程安全的。如果将一个fd加入select()且突然想要关闭该文件描述符,则程序会崩溃

1997年,poll()函数被发明。poll()修复了select()的一些问题,例如:

    -poll()去除了监控上限1024个

    -poll()不再修改传入的数据

poll()的用法:

    函数poll()

    所需头文件:#include<poll.h>

    函数原型:int poll(struct pollfd *fds, nfds_t nfds, int timeout);

    函数参数:

        fds        需要参与测试的文件描述符数组。该参数是一个struct pollfd类型的结构体数组,该结构体如下:

struct pollfd

            {undefined

                int fd;            //需要测试的文件描述符

                short events;    //需要测试的事件

                short revents;    //实际发生的事件(即返回结果)

            }

            events与revents的取值如下:

                POLLIN        是否有数据可读

                POLLOUT        是否有数据可写

                POLLERR        是否发生错误(仅能检测输出用文件描述符)

                POLLHUP        是否被挂起(Hang up,仅能检测输出用文件描述符)

                POLLNVALfd    是否是一个打开的文件(仅能检测输出用文件描述符)

        nfds    fds数组内结构体的数量

        timeout    超时时间,单位为毫秒

    函数返回值:

        成功:>0    数组fds中检测成功的文件描述符的总数量

        失败:

            0      超时

            -1    错误

但是poll()仍然不是线程安全的。于是2002年,David Libenzi发明了poll()的取代方案epoll()

epoll()可以视为多路IO复用的最终实现,它修复了select()和poll()的绝大多数问题,例如:

    -epoll()是线程安全的,因此无需考虑线程互斥

    -在没有数据准备就绪时,epoll()不会轮询(即不占用CPU),而select()则占用CPU

    -epoll()不仅返回有数据准备就绪,还直接返回对应的文件描述符,因此无需额外的选择代码

//注意:epoll()并不是一个函数而是一个函数族,包括epoll_create()、epoll_create1()、epoll_ctl()、epoll_wait()等函数

//epoll()本质是添加了信号驱动型IO的多路复用,在内核中使用mmap共享用户空间与内核空间,并使用回调函数。在这里不再展开描述

//epoll()唯一的缺点是只有Linux可用......
 


Socket编程常用接口

inet_ntop()

inet_pton()

setsockopt()


int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
这两个接口可以通过套接字描述符来获取这个连接的对端的sockaddr_in结构体

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值