网站如何做团购,网站域名做链接怎么做,wordpress 花生壳,肥乡企业做网站推广目录 网络通信概述网络互连模型#xff1a;OSI 七层模型TCP/IP 四层/五层模型数据的封装与拆封 IP 地址IP 地址的编址方式IP 地址的分类特殊的IP 地址如何判断2 个IP 地址是否在同一个网段内 TCP/IP 协议TCP 协议TCP 协议的特性TCP 报文格式建立TCP 连接#xff1a;三次握手关… 目录 网络通信概述网络互连模型OSI 七层模型TCP/IP 四层/五层模型数据的封装与拆封 IP 地址IP 地址的编址方式IP 地址的分类特殊的IP 地址如何判断2 个IP 地址是否在同一个网段内 TCP/IP 协议TCP 协议TCP 协议的特性TCP 报文格式建立TCP 连接三次握手关闭TCP 连接四次挥手TCP 状态说明UDP 协议 端口号的概念socket 简介socket 编程接口介绍socket()函数bind()函数listen()函数accept()函数connect()函数发送和接收函数close()关闭套接字 IP 地址格式转换函数inet_aton、inet_addr、inet_ntoa 函数inet_ntop、inet_pton 函数 socket 编程实战编写服务器程序编写客户端程序 Linux 系统是依靠互联网平台迅速发展起来的所以它具有强大的网络功能支持也是Linux 系统的一大特点。互联网对人类社会产生了巨大影响它几乎改变了人们生活的方方面面可见互联网对人类社会的重要性 本章我们便来学习一些网络基础知识为下一章学习网络编程打下一个基础本章会向大家介绍网络基础知识譬如网络通信概述、OSI 七层模型、IP 地址、TCP/IP 协议族、TCP 和UDP 协议等等其中并不会深入、详细地介绍这些内容旨在以引导大家入门、了解为主如果感兴趣的读者可以自行查阅相关书籍进行深入学习。
网络通信概述
网络通信本质上是一种进程间通信是位于网络中不同主机上的进程之间的通信属于IPC 的一种通常称为socket IPC在第十章中给大家简单地提到过如图10.2.1 中所示。所以网络通信是为了解决在网络环境中不同主机上的应用程序之间的通信问题。 大概可以分为三个层次如下所示 1、硬件层网卡设备收发网络数据 2、驱动层网卡驱动Linux 内核网卡驱动代码 3、应用层上层应用程序调用socket 接口或更高级别接口实现网络相关应用程序
在硬件上两台主机都提供了网卡设备也就满足了进行网络通信最基本的要求网卡设备是实现网络数据收发的硬件基础。并且通信的两台主机之间需要建立网络连接这样两台主机之间才可以进行数据传输譬如通过网线进行数据传输。网络数据的传输媒介有很多种大体上分为有线传输譬如双绞线网线、光纤等和无线传输譬如WIFI、蓝牙、ZigBee、4G/5G/GPRS 等PC 机通常使用有线网络而手机等移动设备通常使用无线网络。 在内核层提供了网卡驱动程序可以驱动底层网卡硬件设备同时向应用层提供socket 接口。 在应用层应用程序基于内核提供的socket 接口进行应用编程实现自己的网络应用程序。需要注意的是socket 接口是内核向应用层提供的一套网络编程接口所以我们学习网络编程其实就是学习socket 编程如何基于socket 接口编写应用程序。 除了socket 接口之外在应用层通常还会使用一些更为高级的编程接口譬如http、网络控件等那么这些接口实际上是对socket 接口的一种更高级别的封装。在正式学习socket 编程之前我们需要先了解一些网络基础知识为后面的学习打下一个理论基础。 网络通信知识庞大其中涉及到一大堆的网络协议TCP/IP 协议族笔者不可能把这些内容给大家介绍清楚本章仅仅只是进行简单介绍以了解为目的。
网络互连模型OSI 七层模型
七层模型亦称OSIOpen System Interconnection。OSI 七层参考模型是国际标准化组织ISO制定的一个用于计算机或通信系统间网络互联的标准体系一般称为OSI 参考模型或七层模型。OSI 七层模型是一个网络互连模型从上到下依次是 从上可知网络通信的模型分了很多层为什么需要分为这么多层次原因就在于网络是一种非常复杂的通信需要进行分层每一层需要去实现不同的功能。下面我们来详细看下OSI 参考模型中每一层的作用。 应用层 应用层Application Layer是OSI 参考模型中的最高层是最靠近用户的一层为上层用户提供应用接口也为用户直接提供各种网络服务。我们常见应用层的网络服务协议有HTTP、FTP、TFTP、SMTP、 SNMP、DNS、TELNET、HTTPS、POP3、DHCP。 表示层 表示层Presentation Layer提供各种用于应用层数据的编码和转换功能确保一个系统的应用层发送的数据能被另一个系统的应用层识别。如果必要该层可提供一种标准表示形式用于将计算机内部的多种数据格式转换成通信中采用的标准表示形式。数据压缩/解压缩和加密/解密提供网络的安全性也是表示层可提供的功能之一。 会话层 会话层Session Layer对应主机进程指本地主机与远程主机正在进行的会话。会话层就是负责建立、管理和终止表示层实体之间的通信会话。该层的通信由不同设备中的应用程序之间的服务请求和响应组成。将不同实体之间表示层的连接称为会话。因此会话层的任务就是组织和协调两个会话进程之间的通信并对数据交换进行管理。 传输层 传输层Transport Layer定义传输数据的协议端口号以及端到端的流控和差错校验。该层建立了主机端到端的连接传输层的作用是为上层协议提供端到端的可靠和透明的数据传输服务包括差错校验处理和流控等问题。我们通常说的TCP、UDP 协议就工作在这一层端口号既是这里的“端”。 网络层 进行逻辑地址寻址实现不同网络之间的路径选择。本层通过IP 寻址来建立两个节点之间的连接为源端发送的数据包选择合适的路由和交换节点正确无误地按照地址传送给目的端的运输层。网络层Network Layer也就是通常说的IP 层。该层包含的协议有IPIpv4、Ipv6、ICMP、IGMP 等。 数据链路层 数据链路层Data Link Layer是OSI 参考模型中的第二层负责建立和管理节点间逻辑连接、进行硬件地址寻址、差错检测等功能。将比特组合成字节进而组合成帧用MAC 地址访问介质错误发现但不能纠正。 数据链路层又分为2 个子层逻辑链路控制子层LLC和媒体访问控制子层MAC。MAC 子层的主要任务是解决共享型网络中多用户对信道竞争的问题完成网络介质的访问控制LLC 子层的主要任务是建立和维护网络连接执行差错校验、流量控制和链路控制。 数据链路层的具体工作是接收来自物理层的位流形式的数据并封装成帧传送到上一层同样也将来自上层的数据帧拆装为位流形式的数据转发到物理层并且还负责处理接收端发回的确认帧的信息以便提供可靠的数据传输。 物理层 物理层Physical Layer是OSI 参考模型的最低层物理层的主要功能是利用传输介质为数据链路层提供物理连接实现比特流的透明传输物理层的作用是实现相邻计算机节点之间比特流的透明传送尽可能屏蔽掉具体传输介质和物理设备的差异。使数据链路层不必考虑网络的具体传输介质是什么。“透明传送比特流”表示经实际电路传送后的比特流没有发生变化对传送的比特流来说这个电路好像是看不见的。 实际上网络数据信号的传输是通过物理层实现的通过物理介质传输比特流。物理层规定了物理设备标准、电平、传输速率等。常用设备有各种物理设备集线器、中继器、调制解调器、网线、双绞线、同轴电缆等这些都是物理层的传输介质。 以上便是对OSI 参考模型中的各个层进行的简单介绍网上也有很多文章对OSI 参考模型做过详细地介绍。除了OSI 七层模型之外大家可能还听过TCP/IP 四层模型、TCP/IP 五层模型那么这些又是什么呢接下来将向大家介绍。
TCP/IP 四层/五层模型
事实上TCP/IP 模型是OSI 模型的简化版本我们来看看OSI 七层模型和TCP/IP 五层模型之间的对应的关系
所以由上图可知TCP/IP 五层模型中将OSI 七层模型的最上三层应用层、表示层和会话层合并为一个层即应用层所以TCP/IP 五层模型包括应用层、传输层、网络层、数据链路层以及物理层。除了TCP/IP 五层模型外还有TCP/IP 四层模型与五层模型唯一不同的就是将数据链路层和物理层合并为网络接口层如下图所示 由上图可知四层模型包括应用层、传输层、网络层以及网络接口层。而在实际的应用中还是使用 TCP/IP 四层模型五层模型是专门为介绍网络原理而设计的。
数据的封装与拆封
网络通信中数据从上层到下层交付时要进行封装同理当目标主机接收到数据时数据由下层传递给上层时需要进行拆封。这就是数据的封装与拆封。 数据的封装过程如下图所示 当用户发送数据时将数据向下交给传输层但是在交给传输层之前应用层相关协议会对用户数据进行封装譬如MQTT、HTTP 等协议其实就是在用户数据前添加一个应用程序头部这是处于应用层的操作最后应用层通过调用传输层接口来将封装好的数据交给传输层。 传输层会在数据前面加上传输层首部此处以TCP 协议为例图中的传输层首部为TCP 首部也可以是UDP 首部然后向下交给网络层。 同样地网络层会在数据前面加上网络层首部IP 首部然后将数据向下交给链路层链路层会对数据进行最后一次封装即在数据前面加上链路层首部此处使用以太网接口为例对应以太网首部然后将数据交给网卡。 最后由网卡硬件设备将数据转换成物理链路上的电平信号数据就这样被发送到了网络中。这就是网络数据的发送过程从图中可以看到各层协议均会对数据进行相应的封装可以概括为TCP/IP 模型中的各层协议对数据进行封装的过程。 以上便是网络数据的封装过程当数据被目标主机接收到之后会进行相反的拆封过程将每一层的首部进行拆解最终得到用户数据。所以数据的接收过程与发送过程正好相反可以概括为TCP/IP 模型中的各层协议对数据进行解析的过程。
IP 地址
Internet 依靠TCP/IP 协议在全球范围内实现不同硬件结构、不同操作系统、不同网络系统的主机之间的互联。在Internet 上每一个节点都依靠唯一的IP 地址相互区分和相互联系IP 地址用于标识互联网中的每台主机的身份设计人员为每个接入网络中的主机都分配一个IP 地址Internet Protocol Address只有合法的IP 地址才能接入互联网中并且与其他主机进行网络通信IP 地址是软件地址不是硬件地址硬件MAC 地址是存储在网卡中的应用于局域网中寻找目标主机。
IP 地址的编址方式
互联网中的每一台主机都需要一个唯一的IP 地址以标识自己的身份那么IP 地址究竟是什么如何去定义一个IP 呢我们需要对IP 地址的编址方式进行了解。 传统的IP 地址是一个32 位二进制数的地址也叫IPv4 地址由4 个8 位字段组成。除了IPv4 之外还有IPv6IPv6 采用128 位地址长度8 个16 位字段组成本小节我们暂时不去理会IPv6 地址。 在网络通信数据包中IP 地址以32 位二进制的形式表示而在人机交互中通常使用点分十进制方式表示譬如192.168.1.1这就是点分十进制的表示方式。 IP 地址中的32 位实际上包含2 部分分别为网络地址和主机地址可通过子网掩码来确定网络地址和主机地址分别占用多少位。
IP 地址的分类
根据IP 地址中网络地址和主机地址两部分分别占多少位的不同将IP 地址划分为5 类分别为A、 B、C、D、E 五类如下所示 1、A 类IP 地址 从上图中可以看到一个A 类IP 地址由1 个字节网络地址和3 个字节主机地址组成而网络地址的最高位必须为0因此可知网络地址取值范围为0~127一共128 个网络地址。当然这128 个网络地址中其中3 个网络地址用作特殊用途因此可用的网络地址有125 个。 (1)、A 类地址的第一字节为网络地址其它3 个字节为主机地址 (2)、A 类地址范围为1.0.0.1 ~ 127.255.255.254 (3)、A 类地址中设有私有地址和保留地址 ①、10.X.X.X 是私有地址所谓私有地址就是在互联网中不能使用而被用在局域网中使用的地址。 ②、127.X.X.X 是保留地址用作循环测试使用。 2、B 类IP 地址 一个B 类IP 地址由2 个字节的网络地址和2 个字节的主机地址组成网络地址的最高位必须是“10”因此网络地址第一个字节的取值范围为128~191IP 地址范围从128.0.0.0 到191.255.255.255。对于B 类地址来说一共拥有16384 个网络地址其中可用的网络地址有16382 个每个网络地址能容纳约6 万2^16-265534多个主机。 (1)、B 类地址中第1 字节和第2 字节为网络地址其它2 个字节为主机地址。 (2)、B 类地址范围128.0.0.1 ~ 191.255.255.254。 (3)、B 类地址中设有私有地址和保留地址 ①、172.16.0.0 ~ 172.31.255.255 是私有地址 ②、169.254.X.X 是保留地址。如果你的IP 地址是自动获取IP 地址而你在网络上又没有找到可用的 DHCP 服务器。就会得到其中一个IP。 3、C 类IP 地址 一个C 类IP 地址由3 字节的网络地址和1 字节的主机地址组成网络地址的最高位必须是“110”因此C 类IP 地址的第一个字节的取值范围为192~223。范围从192.0.0.0 到223.255.255.255网络地址可达 209 万余个每个网络地址能容纳254 个主机。 (1)、C 类地址第1 字节、第2 字节和第3 个字节为网络地址第4 个个字节为主机地址。另外第1 个字节的高三位固定为110。 (2)、C 类地址范围为192.0.0.1 ~ 223.255.255.254。
(3)、C 类地址中的私有地址192.168.X.X 是私有地址。 4、D 类IP 地址 D 类IP 地址第一个字节以“1110”开始它是一个专门保留的地址它并不指向特定的网络目前这一类地址被用在多点广播多播Multicast多点广播地址用来一次寻址一组计算机它标识共享同一协议的一组计算机。 (1)、D 类地址不分网络地址和主机地址它的第1 个字节的高四位固定为1110。 (2)、D 类地址范围224.0.0.1 ~ 239.255.255.254。 5、E 类IP 地址 E 类IP 地址以“llll0”开始为将来使用保留。全零(“0.0.0.0”)地址对应于当前主机。全“1”的IP 地址(“255.255.255.255”)是当前子网的广播地址。 (1)、E 类地址也不分网络地址和主机地址它的第1 个字节的前五位固定为11110。 (2)、E 类地址范围240.0.0.1 ~ 255.255.255.254。 总结 以上就给大家介绍了这5 类IP 地址其中在A、B、C 三类地址中各保留了一个区域作为私有地址 A 类地址10.0.0.010.255.255.255 B 类地址172.16.0.0172.31.255.255 C 类地址192.168.0.0192.168.255.255 A 类地址的第一组数字为1126。 B 类地址的第一组数字为128191。 C 类地址的第一组数字为192223。 A 类地址的表示范围为0.0.0.0~126.255.255.255默认网络掩码为255.0.0.0A 类地址分配给规模特别大的网络使用。A 类地址用第一组数字表示网络地址后面三组数字作为连接于网络上的主机对应的地址。分配给具有大量主机而局域网络个数较少的大型网络譬如IBM 公司的网络。 B 类地址的表示范围为128.0.0.0~191.255.255.255默认网络掩码为255.255.0.0B 类地址分配给一般的中型网络。B 类地址用第一、二组数字表示网络地址后面两组数字代表网络上的主机地址。 C 类地址的表示范围为192.0.0.0~223.255.255.255默认网络掩码为255.255.255.0C 类地址分配给小型网络如一般的局域网和校园网它可连接的主机数量是最少的采用把所属的用户分为若干的网段进行管理。C 类地址用前三组数字表示网络地址最后一组数字作为网络上的主机地址。
特殊的IP 地址
下面给大家介绍一些特殊的IP 地址这些IP 地址不能分配给任何一个网络的主机使用。 直接广播地址 直接广播Direct Broadcast Address向某个网络上所有的主机发送报文。TCP/IP 规定主机号各位全部为“1”的IP 地址用于广播叫作广播地址。譬如一个IP 地址是192.168.0.181这是一个C 类地址所以它的主机号只有一个字节那么对主机号全取1 得到一个广播地址192.168.0.255向这个地址发送数据就能让同一网络下的所有主机接收到。 A、B、C 三类地址的广播地址结构如下 ⚫ A 类地址的广播地址为XXX.255.255.255XXX 为A 类地址中网络地址对应的取值范围譬如 120.255.255.255。 ⚫ B 类地址的广播地址为XXX.XXX.255.255XXX 为B 类地址中网络地址的取值范围譬如 139.22.255.255。 ⚫ C 类地址的广播地址为XXX.XXX.XXX.255XXX 为C 类地址中网络地址的取值范围譬如 203.120.16.255。 受限广播地址 直接广播要求发送方必须广播网络对应的网络号。但有些主机在启动时往往并不知道本网络的网络号这时候如果想要向本网络广播只能采用受限广播地址Limited Broadcast Address。 受限广播地址是在本网络内部进行广播的一种广播地址TCP/IP 规定32 比特全为“1”的IP 地址用于本网络内的广播也就是255.255.255.255。 多播地址 多播地址用在一对多的通信中即一个发送者多个接收者不论接受者数量的多少发送者只发送一次数据包。多播地址属于D 类地址D 类地址只能用作目的地址而不能作为主机中的源地址。 环回地址 环回地址Loopback Address是用于网络软件测试以及本机进程之间通信的特殊地址。把A 类地址中的127.XXX.XXX.XXX 的所有地址都称为环回地址主要用来测试网络协议是否工作正常的作用。比如在电脑中使用ping 命令去ping 127.1.1.1 就可以测试本地TCP/IP 协议是否正常。 不能将环回地址作为任何一台主机的IP 地址使用。 0.0.0.0 地址 IP 地址32bit 全为0 的地址也就是0.0.0.0表示本网络上的本主机只能用作源地址。 0.0.0.0 是不能被ping 通的在服务器中0.0.0.0 并不是一个真实的的IP 地址它表示本机中所有的 IPv4 地址。监听0.0.0.0 的端口就是监听本机中所有IP 的端口。
如何判断2 个IP 地址是否在同一个网段内
如何判断两个IP 地址是否处于同一个子网可通过网络标识来进行判断网络标识定义如下 网络标识 IP 地址 子网掩码 2 个IP 地址的网络标识相同那么它们就处于同一网络。譬如192.168.1.50 和192.168.1.100这2 个都是C 类地址对应的子网掩码为255.255.255.0很明显这两个IP 地址与子网掩码进行按位与操作时得到的结果网络标识是一样的所以它们处于同一网络。
TCP/IP 协议
首先给大家说明的是TCP/IP 协议它其实是一个协议族包含了众多的协议譬如应用层协议HTTP、 FTP、MQTT…以及传输层协议TCP、UDP 等这些都属于TCP/IP 协议可参考图29.2.1 所示。 所以我们一般说TCP/IP 协议它不是指某一个具体的网络协议而是一个协议族。网络通信当中涉及到的网络协议实在太多了对于应用开发来说可能使用更多的是应用层协议譬如HTTP、FTP、SMTP 等。 HTTP 协议 HTTP 超文本传输协议英文HyperText Transfer Protocol缩写HTTP是一种用于分布式、协作式和超媒体信息系统的应用层协议。HTTP 是万维网数据通信的基础。HTTP 的应用最为广泛譬如大家经常会打开网页浏览器查询资料通过浏览器便可开启HTTP 通信。 HTTP 协议工作于客户端用户、服务器端网站模式下浏览器作为HTTP 客户端通过URL 向 HTTP 服务端即WEB 服务器发送请求。Web 服务器根据接收到的请求后向客户端发送响应信息。借助这种浏览器和服务器之间的HTTP 通信我们能够足不出户地获取网络中的各种信息。 FTP 协议 FTP 协议的英文全称为File Transfer Protocol简称为FTP它是一种文件传输协议从一个主机向一个主机传输文件的协议。FTP 协议同样也是基于客户端-服务器模式在客户端和服务器之间进行文件传输譬如我们通常会使用FTP 协议在两台主机之间进行文件传输譬如一台Ubuntu 系统主机和一台Windows 系统主机将一台主机作为FTP 服务器、另一台主机作为FTP 客户端建立FTP 连接之后客户端可以从服务器下载文件同样也可以将文件上传至服务器。 FTP 除了基本的文件上传/下载功能外还有目录操作、权限设置、身份验证等机制许多网盘的文件传输功能都是基于FTP 实现的。 其它的TCP/IP 协议就不给大家介绍了有兴趣的读者可以自行百度了解。 下小节我们重点给大家介绍下工作在传输层的TCP、UDP 协议这两种协议相比各位读者听得比较多。
TCP 协议
TCPTransmission Control Protocol传输控制协议是一种面向连接的、可靠的、基于IP 的传输协议。由图29.2.1 可知TCP 协议工作在传输层对上服务socket 接口对下调用IP 层网络层。 关于TCP 协议我们需要理解的重点如下 ①、TCP 协议工作在传输层对上服务socket 接口对下调用IP 层 ②、TCP 是一种面向连接的传输协议通信之前必须通过三次握手与客户端建立连接关系后才可通信 ③、TCP 协议提供可靠传输不怕丢包、乱序。 TCP 协议如何保证可靠传输 ①、TCP 协议采用发送应答机制即发送端发送的每个TCP 报文段都必须得到接收方的应答才能认为这个TCP 报文段传输成功。 ②、TCP 协议采用超时重传机制发送端在发送出一个TCP 报文段之后启动定时器如果在定时时间内未收到应答它将重新发送该报文段。 ③、由于TCP 报文段最终是以IP 数据报发送的而IP 数据报到达接收端可能乱序、重复、所以TCP 协议还会将接收到的TCP 报文段重排、整理、再交付给应用层。
TCP 协议的特性
TCP 协议的特点如下所示 ⚫ 面向连接的 TCP 是一个面向连接的协议无论哪一方向另一方发送数据之前都必须先在双方之间建立一个TCP 连接否则将无法发送数据通过三次握手建立连接后面在介绍。 ⚫ 确认与重传 当数据从主机A 发送到主机B 时主机B 会返回给主机A 一个确认应答TCP 通过确认应答ACK 实现可靠的数据传输。当发送端将数据发送出去之后会等待对端的确认应答。如果有确认应答说明数据已经成功到达对端。反之数据丢失的可能性比较大。 在一定的时间内如果没有收到确认应答发送端就可以认为数据已经丢失并进行重发。由此即使产生了丢失仍然可以保证数据能够到达对端实现可靠传输。 ⚫ 全双工通信 TCP 连接一旦建立就可以在连接上进行双向的通信。任何一个主机都可以向另一个主机发送数据数据是双向流通的所以TCP 协议是一个全双工的协议。 ⚫ 基于字节流而非报文 将数据按字节大小进行编号接收端通过ACK 来确认收到的数据编号通过这种机制能够保证TCP 协议的有序性和完整性因此TCP 能够提供可靠性传输。 ⚫ 流量控制滑动窗口协议 TCP 流量控制主要是针对接收端的处理速度不如发送端发送速度快的问题消除发送方使接收方缓存溢出的可能性。TCP 流量控制主要使用滑动窗口协议滑动窗口是接受数据端使用的窗口大小用来告诉发送端接收端的缓存大小以此可以控制发送端发送数据的大小从而达到流量控制的目的。这个窗口大小就是我们一次传输几个数据。对所有数据帧按顺序赋予编号发送方在发送过程中始终保持着一个发送窗口只有落在发送窗口内的帧才允许被发送同时接收方也维持着一个接收窗口只有落在接收窗口内的帧才允许接收。这样通过调整发送方窗口和接收方窗口的大小可以实现流量控制。 ⚫ 差错控制 TCP 协议除了确认应答与重传机制外TCP 协议也会采用校验和的方式来检验数据的有效性主机在接收数据的时候会将重复的报文丢弃将乱序的报文重组发现某段报文丢失了会请求发送方进行重发因此在TCP 往上层协议递交的数据是顺序的、无差错的完整数据。 ⚫ 拥塞控制 如果网络上的负载发送到网络上的分组数大于网络上的容量网络同时能处理的分组数就可能引起拥塞判断网络拥塞的两个因素延时和吞吐量。拥塞控制机制是开环预防和闭环消除。 流量控制是通过接收方来控制流量的一种方式而拥塞控制则是通过发送方来控制流量的一种方式。 TCP 发送方可能因为IP 网络的拥塞而被遏制TCP 拥塞控制就是为了解决这个问题注意和TCP 流量控制的区别。 TCP 拥塞控制的几种方法慢启动拥塞避免快重传和快恢复。
TCP 报文格式
从图29.2.4 可知当数据由上层发送到传输层时数据会被封装为TCP 数据段我们将其称为TCP 报文或TCP 报文段TCP 报文由TCP 首部数据区域组成一般TCP 首部通常为20 个字节大小具体格式如下图所示 下面分别对其中的字段进行介绍 源端口号和目标端口号 源端口号和目标端口号各占2 个字节一个4 个字节关于端口号的概念会在29.5.3 小节进行介绍。每个TCP 报文都包含源主机和目标主机的端口号用于寻找发送端和接收端应用进程这两个值加上IP 首部中的源IP 地址和目标IP 地址就能确定唯一一个TCP 连接。有时一个IP 地址和一个端口号也称为socket 插口。 序号 占4 个字节用来标识从TCP 发送端向TCP 接收端发送的数据字节流它的值表示在这个报文段中的第一个数据字节所处位置码根据接收到的数据区域长度就能计算出报文最后一个数据所处的序号因为 TCP 协议会对发送或者接收的数据进行编号按字节的形式那么使用序号对每个字节进行计数就能很轻易管理这些数据。 在TCP 传送的数据流中每一个字节都有一个序号。例如一报文段的序号为300而且数据共100 字节则下一个报文段的序号就是400序号是32bit 的无符号数序号到达2^32-1 后从0 开始。 确认序号 确认序号占4 字节是期望收到对方下次发送的数据的第一个字节的序号也就是期望收到的下一个报文段的首部中的序号确认序号应该是上次已成功收到数据字节序号1。只有ACK 标志为1 时确认序号才有效。TCP 为应用层提供全双工服务这意味数据能在两个方向上独立地进行传输因此确认序号通常会与反向数据即接收端传输给发送端的数据封装在同一个报文中即捎带所以连接的每一端都必须保持每个方向上的传输数据序号准确性。 首部长度 首部长度字段占4 个bit 位它指出了TCP 报文段首部长度以字节为单位最大能记录15*460 字节的首部长度因此TCP 报文段首部最大长度为60 字节。在字段后接下来有6bit 空间是保留未用的供以后应用现在置为0。 6 个标志位URG/ACK/PSH/RST/SYN/FIN 保留位之后有6 个标志位分别如下
①、URG首部中的紧急指针字段标志如果是1 表示紧急指针字段有效。 ②、ACK只有当ACK1 时确认序号字段才有效。 ③、PSH当PSH1 时接收方应该尽快将本报文段立即传送给其应用层。 ④、RST当RST1 时表示出现连接错误必须释放连接然后再重建传输连接。复位比特还用来拒绝一个不法的报文段或拒绝打开一个连接。 ⑤、SYNSYN1ACK0 时表示请求建立一个连接携带SYN 标志的TCP 报文段为同步报文段。 ⑥、FIN为1 表示发送方没有数据要传输了要求释放连接。 窗口大小 占用2 个字节大小表示从确认号开始本报文的发送方可以接收的字节数即接收窗口大小用于流量控制。 校验和 对整个的TCP 报文段包括TCP 首部和TCP 数据以16 位字进行计算所得。这是一个强制性的字段。 紧急指针 本报文段中的紧急数据的最后一个字节的序号。 选项 选项字段的大小是不确定的最多40 字节。
建立TCP 连接三次握手
前面我们提到过TCP 协议是一个面向连接的协议双方在进行网络通信之间都必须先在双方之间建立一条连接俗称“握手”可能在学习网络编程之前大家或多或少都听过“三次握手”、“四次挥手”这些词语那么“三次握手”、“四次挥手”究竟是什么意思本小节将详细讨论一个TCP 连接是如何建立的需要经过哪些过程。 “三次握手”其实是指建立TCP 连接的一个过程通信双方建立一个TCP 连接需要经过“三次握手”这样一个过程。 首先建立连接的过程是由客户端发起而服务器会时刻监听、等待着客户端的连接其示意图如下所示 TCP 连接一般来说会经历以下过程 ⚫ 第一次握手 客户端将TCP 报文标志位SYN 置为1随机产生一个序号值seqJ保存在TCP 首部的序列号(Sequence Number)字段里指明客户端打算连接的服务器的端口并将该数据包发送给服务器端发送完毕后客户端进入SYN_SENT 状态等待服务器端确认。 ⚫ 第二次握手 服务器端收到数据包后由标志位SYN1 知道客户端请求建立连接服务器端将TCP 报文标志位SYN 和ACK 都置为1ackJ1随机产生一个序号值seqK并将该数据包发送给客户端以确认连接请求服务器端进入SYN_RCVD 状态。 ⚫ 第三次握手 客户端收到确认后检查ack 是否为J1ACK 是否为1如果正确则将标志位ACK 置为1ackK1并将该数据包发送给服务器端服务器端检查ack 是否为K1ACK 是否为1如果正确则连接建立成功客户端和服务器端进入ESTABLISHED 状态完成三次握手随后客户端与服务器端之间可以开始传输数据了。 注意:上面写的ack 和ACK不是同一个概念 小写的ack 代表的是头部的确认号Acknowledge numberack。 大写的ACK则是TCP 首部的标志位用于标志的TCP 包是否对上一个包进行了确认操作如果确认了则把ACK 标志位设置成1。 在完成握手后客户端与服务器就成功建立了连接同时双方都得到了彼此的窗口大小序列号等信息在传输TCP 报文段的时候每个TCP 报文段首部的SYN 标志都会被置0因为它只用于发起连接同步序号。 为什么需要三次握手 其实TCP 三次握手过程跟现实生活中的人与人之间的电话交流是很类似的譬如A 打电话给B A“喂你能听到我的声音吗” B“我听得到呀你能听到我的声音吗” A“我能听到你………” …… 经过三次的互相确认大家就会认为对方对听的到自己说话才开始接下来的沟通交流否则如果不进行确认那么你在说话的时候对方不一定能听到你的声音。所以TCP 的三次握手是为了保证传输的安全、可靠。
关闭TCP 连接四次挥手
除了“三次握手”还有“四次挥手”“四次挥手”有一些书也会称为四次握手其实是指关闭TCP 连接的一个过程当通信双方需要关闭TCP 连接时需要经过“四次挥手”这样一个过程。 四次挥手即终止TCP 连接就是指断开一个TCP 连接时需要客户端和服务端总共发送4 个包以确认连接的断开。在socket 编程中这一过程由客户端或服务端任一方执行close 来触发。 由于TCP 连接是全双工的因此每个方向都必须要单独进行关闭这一原则是当一方完成数据发送任务后发送一个FIN 来终止这一方向的连接收到一个FIN 只是意味着这一方向上没有数据流动了即不会再收到数据了但是在这个TCP 连接上仍然能够发送数据直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭而另一方则执行被动关闭。 四次挥手过程的示意图如下
挥手请求可以是Client 端也可以是Server 端发起的我们假设是Client 端发起 ⚫ 第一次挥手 Client 端发起挥手请求向Server 端发出一个FIN 报文段主动进行关闭连接此时报文段的FIN 标志位被设置为1。此时Client 端进入FIN_WAIT_1 状态这表示Client 端没有数据要发送给Server 端了。 ⚫ 第二次挥手 Server 端收到了Client 端发送的FIN 报文段向Client 端返回一个ACK 报文段此时报文段的ACK 标志位被设置为1。ack 设为seq 加1Client 端进入FIN_WAIT_2 状态Server 端告诉Client 端我确认并同意你的关闭请求。 ⚫ 第三次挥手 Server 端向Client 端发送一个FIN 报文段请求关闭连接此时报文段的FIN 标志位被设置为1同时 Client 端进入LAST_ACK 状态。 ⚫ 第四次挥手 Client 端收到Server 端发送的FIN 报文段后向Server 端发送ACK 报文段此时报文段的ACK 标志位被设置为1然后Client 端进入TIME_WAIT 状态。Server 端收到Client 端的ACK 报文段以后就关闭连接。此时Client 端等待2MSL 的时间后依然没有收到回复则证明Server 端已正常关闭那好Client 端也可以关闭连接了。 这就是关闭TCP 连接的四次挥手过程。所以TCP 协议传输数据的整个过程就如同下图所示
在正式进行数据传输之前需要先建立连接当成功建立TCP 连接之后双方就可以进行数据传输了。当不再需要传输数据时关闭连接即可
TCP 状态说明
TCP 协议在建立连接、断开连接以及数据传输过程中都会呈现出现不同的状态不同的状态采取的动作也是不同的需要处理各个状态之间的关系。图29.5.2、图29.5.3 以及图29.5.4 中就出现了一些状态标志除了这些状态标志之外还有其它一些TCP 状态对这些TCP 状态的说明如下所示 ⚫ CLOSED 状态表示一个初始状态。 ⚫ LISTENING 状态这是一个非常容易理解的状态表示服务器端的某个SOCKET 处于监听状态监听客户端的连接请求可以接受连接了。譬如服务器能够提供某种服务它会监听客户端TCP 端口的连接请求处于LISTENING 状态端口是开放的等待被客户端连接。 ⚫ SYN_SENT 状态(客户端状态)当客户端调用connect()函数连接时它首先会发送SYN 报文给服务器请求建立连接因此也随即它会进入到了SYN_SENT 状态并等待服务器的发送三次握手中的第2 个报文。SYN_SENT 状态表示客户端已发送SYN 报文。 ⚫ SYN_REVD 状态(服务端状态)这个状态表示服务器接受到了SYN 报文在正常情况下这个状态是服务器端的SOCKET 在建立TCP 连接时的三次握手过程中的一个中间状态很短暂基本上用netstat 你是很难看到这种状态的除非你特意写了一个客户端测试程序故意将三次TCP 握手过程中最后一个ACK 报文不予发送。因此这种状态时当收到客户端的ACK 报文后它会进入到ESTABLISHED 状态。 ⚫ ESTABLISHED 状态这个容易理解了表示连接已经建立了。 ⚫ FIN_WAIT_1 和FIN_WAIT_2 状态其实FIN_WAIT_1 和FIN_WAIT_2 状态的真正含义都是表示等待对方的FIN 报文。而这两种状态的区别是FIN_WAIT_1 状态实际上是当SOCKET 在 ESTABLISHED 状态时它想主动关闭连接向对方发送了FIN 报文此时该SOCKET 即进入到 FIN_WAIT_1 状态。而当对方回应ACK 报文后则进入到FIN_WAIT_2 状态当然在实际的正常情况下无论对方何种情况下都应该马上回应ACK 报文所以FIN_WAIT_1 状态一般是比较难见到的而FIN_WAIT_2 状态还有时常常可以用netstat 看到。 ⚫ TIME_WAIT 状态表示收到了对方的FIN 报文并发送出了ACK 报文就等2MSL 后即可回到CLOSED 可用状态了。如果FIN_WAIT_1 状态下收到了对方同时带FIN 标志和ACK 标志的报文时可以直接进入到TIME_WAIT 状态而无须经过FIN_WAIT_2 状态。 ⚫ CLOSE_WAIT 状态这种状态的含义其实是表示在等待关闭。怎么理解呢当对方close 一个 SOCKET 后发送FIN 报文给自己你系统毫无疑问地会回应一个ACK 报文给对方此时则进入到 CLOSE_WAIT 状态。接下来呢实际上你真正需要考虑的事情是察看你是否还有数据发送给对方如果没有的话那么你也就可以close 这个SOCKET发送FIN 报文给对方也即关闭连接。所以你在CLOSE_WAIT 状态下需要完成的事情是等待你去关闭连接。 ⚫ LAST_ACK 状态它是被动关闭一方在发送FIN 报文后最后等待对方的ACK 报文。当收到ACK 报文后也即可以进入到CLOSED 状态了。 以上便是关于TCP 状态的一些描述说明状态之间的转换关系就如上图中所示。
UDP 协议
除了TCP 协议外还有UDP 协议想必大家都听过说UDP 是User Datagram Protocol 的简称中文名是用户数据报协议是一种无连接、不可靠的协议同样它也是工作在传顺层。它只是简单地实现从一端主机到另一端主机的数据传输功能这些数据通过IP 层发送在网络中传输到达目标主机的顺序是无法预知的因此需要应用程序对这些数据进行排序处理这就带来了很大的不方便此外UDP 协议更没有 流量控制、拥塞控制等功能在发送的一端UDP 只是把上层应用的数据封装到UDP 报文中在差错检测方面仅仅是对数据进行了简单的校验然后将其封装到IP 数据报中发送出去。而在接收端无论是否收到数据它都不会产生一个应答发送给源主机并且如果接收到数据发送校验错误那么接收端就会丢弃该 UDP 报文也不会告诉源主机这样子传输的数据是无法保障其准确性的如果想要其准确性那么就需要应用程序来保障了。 UDP 协议的特点 ①、无连接、不可靠 ②、尽可能提供交付数据服务出现差错直接丢弃无反馈 ③、面向报文发送方的UDP 拿到上层数据直接添加个UDP 首部然后进行校验后就递交给IP 层而接收的一方在接收到UDP 报文后简单进行校验然后直接去除数据递交给上层应用 ④、速度快因为UDP 协议没有TCP 协议的握手、确认、窗口、重传、拥塞控制等机制UDP 是一个无状态的传输协议所以它在传递数据时非常快即使在网络拥塞的时候UDP 也不会降低发送的数据。 UDP 虽然有很多缺点但也有自己的优点所以它也有很多的应用场合因为在如今的网络环境下 UDP 协议传输出现错误的概率是很小的并且它的实时性是非常好常用于实时视频的传输比如直播、网络电话等因为即使是出现了数据丢失的情况导致视频卡帧这也不是什么大不了的事情所以UDP 协议还是会被应用与对传输速度有要求并且可以容忍出现差错的数据传输中。
端口号的概念
前面给大家介绍了IP 地址互联网中的每一台主机都需要一个唯一的IP 地址以标识自己的身份网络中传输的数据包通过IP 地址找到对应的目标主机一台主机通常只有一个IP 地址但主机上运行的网络进程却通常不止一个譬如Windows 电脑上运行着QQ、微信、钉钉、网页浏览器等这些进程都需要进行网络连接它们都可通过网络发送/接收数据那么这里就有一个问题主机接收到网络数据之后如何确定该数据是哪个进程对应的接收数据呢其实就是通常端口号来确定的。 端口号本质上就是一个数字编号用来在一台主机中唯一标识一个能上网能够进行网络通信的进程端口号的取值范围为0~65535。一台主机通常只有一个IP 地址但是可能有多个端口号每个端口号表示一个能上网的进程。一台拥有IP 地址的主机可以提供许多服务比如Web 服务、FTP 服务、SMTP 服务等这些服务都是能够进行网络通信的进程IP 地址只能区分网络中不同的主机并不能区分主机中的这些进程显然不能只靠IP 地址因此才有了端口号。通过“IP 地址端口号”来区分主机不同的进程。 很多常见的服务器它都有特定的端口号具体详情如下表所示
socket 简介
Linux 系统是依靠互联网平台迅速发展起来的所以它具有强大的网络功能支持也是Linux 系统的一大特点。互联网对人类社会产生了巨大影响它几乎改变了人们生活的方方面面可见互联网对人类社会的重要性 本章我们便来学习Linux 下的网络编程我们一般称为socket 编程在上一章中给大家介绍过socket 是内核向应用层提供的一套网络编程接口用户基于socket 接口可开发自己的网络相关应用程序。 本章作为网络编程基础内容其中并不会深入、详细地介绍socket 编程旨在以引导大家入门为主其一在于网络编程本就是一门非常难、非常深奥的技能市面上有很多关于Linux/UNIX 网络编程类书籍这些书籍专门介绍了网络编程相关知识内容而且书本非常厚可将其内容之多、难点之多其二在于笔者对网络编程了解知之甚少掌握的知识、技能太少无法向大家传授更加深入的知识、内容如果大家以后有机会从事网络编程开发相关工作可以购买此类书籍深入学习、研究。
套接字socket是Linux 下的一种进程间通信机制socket IPC在前面的内容中已经给大家提到过使用socket IPC 可以使得在不同主机上的应用程序之间进行通信网络通信当然也可以是同一台主机上的不同应用程序。socket IPC 通常使用客户端—服务器这种模式完成通信多个客户端可以同时连接到服务器中与服务器之间完成数据交互。 内核向应用层提供了socket 接口对于应用程序开发人员来说我们只需要调用socket 接口开发自己的应用程序即可socket 是应用层与TCP/IP 协议通信的中间软件抽象层它是一组接口。在设计模式中 socket 其实就是一个门面模式它把复杂的TCP/IP 协议隐藏在socket 接口后面对用户来说一组简单的接口就是全部让socket 去组织数据以符合指定的协议。所以我们无需深入的去理解tcp/udp 等各种复杂的TCP/IP 协议socket 已经为我们封装好了我们只需要遵循socket 的规定去编程写出的程序自然遵循tcp/udp 标准的。 当前网络中的主流程序设计都是使用socket 进行编程的因为它简单易用它还是一个标准BSD socket能在不同平台很方便移植比如你的一个应用程序是基于socket 接口编写的那么它可以移植到任何实现BSD socket 标准的平台譬如LwIP它兼容BSD Socket又譬如Windows它也实现了一套基于 socket 的套接字接口更甚至在国产操作系统中如RT-Thread它也实现了BSD socket 标准的socket 接口。
socket 编程接口介绍
本小节我们向大家介绍socket 编程中使用到的一些接口函数。使用socket 接口需要在我们的应用程序代码中包含两个头文件
#include sys/types.h /* See NOTES */
#include sys/socket.hsocket()函数
socket()函数原型如下所示
#include sys/types.h /* See NOTES */
#include sys/socket.h
int socket(int domain, int type, int protocol);socket()函数类似于open()函数它用于创建一个网络通信端点打开一个网络通信如果成功则返回一个网络文件描述符通常把这个文件描述符称为socket 描述符socket descriptor这个socket 描述符跟文件描述符一样后续的操作都有用到它把它作为参数通过它来进行一些读写操作。 该函数包括3 个参数如下所示 domain 参数domain 用于指定一个通信域这将选择将用于通信的协议族。可选的协议族如下表所示 对于TCP/IP 协议来说通常选择AF_INET 就可以了当然如果你的IP 协议的版本支持IPv6那么可以选择AF_INET6。 type 参数type 指定套接字的类型当前支持的类型有 protocol 参数protocol 通常设置为0表示为给定的通信域和套接字类型选择默认协议。当对同一域和套接字类型支持多个协议时可以使用protocol 参数选择一个特定协议。在AF_INET 通信域中套接字类型为 SOCK_STREAM 的默认协议是传输控制协议Transmission Control ProtocolTCP 协议。在AF_INET 通信域中套接字类型为SOCK_DGRAM 的默认协议时UDP。 调用socket()与调用open()函数很类似调用成功情况下均会返回用于文件I/O 的文件描述符只不过对于socket()来说其返回的文件描述符一般称为socket 描述符。当不再需要该文件描述符时可调用 close()函数来关闭套接字释放相应的资源。 如果socket()函数调用失败则会返回-1并且会设置errno 变量以指示错误类型。 使用示例
int socket_fd socket(AF_INET, SOCK_STREAM, 0); // 打开套接字
if (0 socket_fd)
{perror(socket error);exit(-1);
}
............ close(socket_fd); // 关闭套接字
bind()函数
bind()函数原型如下所示 int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); bind()函数用于将一个IP 地址或端口号与一个套接字进行绑定将套接字与地址进行关联。将一个客户端的套接字关联上一个地址没有多少新意可以让系统选一个默认的地址。一般来讲会将一个服务器的套接字绑定到一个众所周知的地址—即一个固定的与服务器进行通信的客户端应用程序提前就知道的地址注意这里说的地址包括IP 地址和端口号。因为对于客户端来说它与服务器进行通信首先需要知道服务器的IP 地址以及对应的端口号所以通常服务器的IP 地址以及端口号都是众所周知的。 调用bind()函数将参数sockfd 指定的套接字与一个地址addr 进行绑定成功返回0失败情况下返回- 1并设置errno 以提示错误原因。 参数addr 是一个指针指向一个struct sockaddr 类型变量如下所示
struct sockaddr {sa_family_t sa_family;char sa_data[14];
}第二个成员sa_data 是一个char 类型数组一共14 个字节在这14 个字节中就包括了IP 地址、端口号等信息这个结构对用户并不友好它把这些信息都封装在了sa_data 数组中这样使得用户是无法对 sa_data 数组进行赋值。事实上这是一个通用的socket 地址结构体。 一般我们在使用的时候都会使用struct sockaddr_in 结构体sockaddr_in 和sockaddr 是并列的结构占用的空间是一样的指向sockaddr_in 的结构体的指针也可以指向sockadd 的结构体并代替它而且 sockaddr_in 结构对用户将更加友好在使用的时候进行类型转换就可以了。该结构体内容如下所示
struct sockaddr_in
{sa_family_t sin_family; /* 协议族*/in_port_t sin_port; /* 端口号*/struct in_addr sin_addr; /* IP 地址*/unsigned char sin_zero[8];
};这个结构体的第一个字段是与sockaddr 结构体是一致的而剩下的字段就是sa_data 数组连续的14 字节信息里面的内容只不过从新定义了成员变量而已sin_port 字段是我们需要填写的端口号信息sin_addr 字段是我们需要填写的IP 地址信息剩下sin_zero 区域的8 字节保留未用。 最后一个参数addrlen 指定了addr 所指向的结构体对应的字节长度。 使用示例
struct sockaddr_in socket_addr;
memset(socket_addr, 0x0, sizeof(socket_addr)); // 清零
// 填充变量
socket_addr.sin_family AF_INET;
socket_addr.sin_addr.s_addr htonl(INADDR_ANY);
socket_addr.sin_port htons(5555);
// 将地址与套接字进行关联、绑定
bind(socket_fd, (struct sockaddr *)socket_addr, sizeof(socket_addr));
注意代码中的htons 和htonl 并不是函数只是一个宏定义主要的作用在于为了避免大小端的问题需要这些宏需要在我们的应用程序代码中包含头文件netinet/in.h。 Tipsbind()函数并不是总是需要调用的只有用户进程想与一个具体的IP 地址或端口号相关联的时候才需要调用这个函数。如果用户进程没有这个必要那么程序可以依赖内核的自动的选址机制来完成自动地址选择通常在客户端应用程序中会这样做。
listen()函数
listen()函数只能在服务器进程中使用让服务器进程进入监听状态等待客户端的连接请求listen()函数在一般在bind()函数之后调用在accept()函数之前调用它的函数原型是 int listen(int sockfd, int backlog); 无法在一个已经连接的套接字即已经成功执行connect()的套接字或由accept()调用返回的套接字上执行listen()。 参数backlog 用来描述sockfd 的等待连接队列能够达到的最大值。在服务器进程正处理客户端连接请求的时候可能还存在其它的客户端请求建立连接因为TCP 连接是一个过程由于同时尝试连接的用户过多使得服务器进程无法快速地完成所有的连接请求那怎么办呢直接丢掉其他客户端的连接肯定不是一个很好的解决方法。因此内核会在自己的进程空间里维护一个队列这些连接请求就会被放入一个队列中服务器进程会按照先来后到的顺序去处理这些连接请求这样的一个队列内核不可能让其任意大所以必须有一个大小的上限这个backlog 参数告诉内核使用这个数值作为队列的上限。而当一个客户端的连接请求到达并且该队列为满时客户端可能会收到一个表示连接失败的错误本次请求会被丢弃不作处理。
accept()函数
服务器调用listen()函数之后就会进入到监听状态等待客户端的连接请求使用accept()函数获取客户端的连接请求并建立连接。函数原型如下所示 int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 为了能够正常让客户端能正常连接到服务器服务器必须遵循以下处理流程 ①、调用socket()函数打开套接字 ②、调用bind()函数将套接字与一个端口号以及IP 地址进行绑定 ③、调用listen()函数让服务器进程进入监听状态监听客户端的连接请求 ④、调用accept()函数处理到来的连接请求。 accept()函数通常只用于服务器应用程序中如果调用accept()函数时并没有客户端请求连接等待连接队列中也没有等待连接的请求此时accept()会进入阻塞状态直到有客户端连接请求到达为止。当有客户端连接请求到达时accept()函数与远程客户端之间建立连接accept()函数返回一个新的套接字。这个套接字与socket()函数返回的套接字并不同socket()函数返回的是服务器的套接字以服务器为例而 accept()函数返回的套接字连接到调用connect()的客户端服务器通过该套接字与客户端进行数据交互譬如向客户端发送数据、或从客户端接收数据。 所以理解accept()函数的关键点在于它会创建一个新的套接字其实这个新的套接字就是与执行 connect()客户端调用connect()向服务器发起连接请求的客户端之间建立了连接这个套接字代表了服务器与客户端的一个连接。如果accept()函数执行出错将会返回-1并会设置errno 以指示错误原因。 参数addr 是一个传出参数参数addr 用来返回已连接的客户端的IP 地址与端口号等这些信息。参数 addrlen 应设置为addr 所指向的对象的字节长度如果我们对客户端的IP 地址与端口号这些信息不感兴趣可以把arrd 和addrlen 均置为空指针NULL。
connect()函数
connect()函数原型如下所示 int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 该函数用于客户端应用程序中客户端调用connect()函数将套接字sockfd 与远程服务器进行连接参数addr 指定了待连接的服务器的IP 地址以及端口号等信息参数addrlen 指定了addr 指向的struct sockaddr 对象的字节大小。 客户端通过connect()函数请求与服务器建立连接对于TCP 连接来说调用该函数将发生TCP 连接的握手过程并最终建立一个TCP 连接而对于UDP 协议来说调用这个函数只是在sockfd 中记录服务器 IP 地址与端口号而不发送任何数据。 函数调用成功则返回0失败返回-1并设置errno 以指示错误原因。
发送和接收函数
一旦客户端与服务器建立好连接之后我们就可以通过套接字描述符来收发数据了对于客户端使用 socket()返回的套接字描述符而对于服务器来说需要使用accept()返回的套接字描述符这与我们读写普通文件是差不多的操作譬如可以调用read()或recv()函数读取网络数据调用write()或send()函数发送数据。 read()函数 read()函数大家都很熟悉了通过read()函数从一个文件描述符中读取指定字节大小的数据并放入到指定的缓冲区中read()调用成功将返回读取到的字节数此返回值受文件剩余字节数限制当返回值小于指定的字节数时并不意味着错误这可能是因为当前可读取的字节数小于指定的字节数比如已经接近文件结尾或者正在从管道或者终端读取数据或者read()函数被信号中断等出错返回-1 并设置errno如果在调read 之前已到达文件末尾则这次read 返回0。 套接字描述符也是文件描述符所以使用read()函数读取网络数据时read()函数的参数fd 就是对应的套接字描述符。 recv()函数 recv()函数原型如下所示 ssize_t recv(int sockfd, void *buf, size_t len, int flags); 不论是客户端还是服务器都可以通过revc()函数读取网络数据它与read()函数的功能是相似的。参数 sockfd 指定套接字描述符参数buf 指向了一个数据接收缓冲区参数len 指定了读取数据的字节大小参数flags 可以指定一些标志用于控制如何接收数据。 函数recv()与read()很相似但是recv()可以通过指定flags 标志来控制如何接收数据这些标志如下所示 通常一般我们将flags 参数设置为0当然你可以根据自己的需求设置该参数。 当指定MSG_PEEK 标志时可以查看下一个要读取的数据但不真正取走它当再次调用read 或recv 函数时会返回刚才查看的数据。 对于SOCK_STREAM 类型套接字接收的数据可以比指定的字节大小少。MSG_WAITALL 标志会阻止这种行为知道所请求的数据全部返回recv 函数才会返回。对于SOCK_DGRAM 和SOCK_SEQPACKET 套接字MSG_WAITALL 标志并不会改变什么行为因为这些基于报文的套接字类型一次读取就返回整个报文。 如果发送者已经调用shutdown 来结束传输或者网络协议支持按默认的顺序关闭并且发送端已经关闭那么当所有的数据接收完毕后recv 会返回0。 recv 在调用成功情况下返回实际读取到的字节数。 write()函数 通过write()函数可以向套接字描述符中写入数据函数调用成功返回写入的字节数失败返回-1并设置errno 变量。 send()函数 函数原型如下所示 ssize_t send(int sockfd, const void *buf, size_t len, int flags); send 和write 很相似但是send 可以通过参数flags 指定一些标志来改变处理传输数据的方式。这些标志如下所示 即使send()成功返回也并不表示连接的另一端的进程就一定接收了数据我们所能保证的只是当send 成功返回时数据已经被无错误的发送到网络驱动程序上。
close()关闭套接字
当不再需要套接字描述符时可调用close()函数来关闭套接字释放相应的资源。
IP 地址格式转换函数
对于人来说我们更容易阅读的是点分十进制的IP 地址譬如192.168.1.110、192.168.1.50这其实是一种字符串的形式但是计算机所需要理解的是二进制形式的IP 地址所以我们就需要在点分十进制字符串和二进制地址之间进行转换。 点分十进制字符串和二进制地址之间的转换函数主要有inet_aton、inet_addr、inet_ntoa、inet_ntop、 inet_pton 这五个在我们的应用程序中使用它们需要包含头文件sys/socket.h 、arpa/inet.h 以及 netinet/in.h。
inet_aton、inet_addr、inet_ntoa 函数
这些函数可将一个IP 地址在点分十进制表示形式和二进制表示形式之间进行转换这些函数已经废弃了基本不用这些函数了但是在一些旧的代码中可能还会看到这些函数。完成此类转换工作我们应该使用下面介绍的这些函数。
inet_ntop、inet_pton 函数
inet_ntop()、inet_pton()与inet_ntoa()、inet_aton()类似但它们还支持IPv6 地址。它们将二进制Ipv4 或 Ipv6 地址转换成以点分十进制表示的字符串形式或将点分十进制表示的字符串形式转换成二进制Ipv4 或 Ipv6 地址。使用这两个函数只需包含arpa/inet.h头文件即可 inet_pton()函数 inet_pton()函数原型如下所示 int inet_pton(int af, const char *src, void *dst); inet_pton()函数将点分十进制表示的字符串形式转换成二进制Ipv4 或Ipv6 地址。 将字符串src 转换为二进制地址参数af 必须是AF_INET 或AF_INET6AF_INET 表示待转换的Ipv4 地址AF_INET6 表示待转换的是Ipv6 地址并将转换后得到的地址存放在参数dst 所指向的对象中如果参数af 被指定为AF_INET则参数dst 所指对象应该是一个struct in_addr 结构体的对象如果参数af 被指定为AF_INET6则参数dst 所指对象应该是一个struct in6_addr 结构体的对象。 inet_pton()转换成功返回1已成功转换。如果src 不包含表示指定地址族中有效网络地址的字符串则返回0。如果af 不包含有效的地址族则返回-1 并将errno 设置为EAFNOSUPPORT。 使用示例
#include stdio.h
#include stdlib.h
#include arpa/inet.h
#define IPV4_ADDR 192.168.1.222
int main(void)
{struct in_addr addr;inet_pton(AF_INET, IPV4_ADDR, addr);printf(ip addr: 0x%x\n, addr.s_addr);exit(0);
}
测试结果 inet_ntop()函数 inet_ntop()函数执行与inet_pton()相反的操作函数原型如下所示 const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); 参数af 与inet_pton()函数的af 参数意义相同。 参数src 应指向一个struct in_addr 结构体对象或struct in6_addr 结构体对象依据参数af 而定。函数 inet_ntop()会将参数src 指向的二进制IP 地址转换为点分十进制形式的字符串并将字符串存放在参数dts 所指的缓冲区中参数size 指定了该缓冲区的大小。 inet_ntop()在成功时会返回dst 指针。如果size 的值太小了那么将会返回NULL 并将errno 设置为 ENOSPC。 使用示例
#include stdio.h
#include stdlib.h
#include arpa/inet.h
int main(void)
{struct in_addr addr;char buf[20] {0};addr.s_addr 0xde01a8c0;inet_ntop(AF_INET, addr, buf, sizeof(buf));printf(ip addr: %s\n, buf);exit(0);
}
测试结果
socket 编程实战
经过上面的介绍本小节我们将进行编程实战实现一个简单地服务器和一个简单地客户端应用程序。
编写服务器程序
编写服务器应用程序的流程如下 ①、调用socket()函数打开套接字得到套接字描述符 ②、调用bind()函数将套接字与IP 地址、端口号进行绑定 ③、调用listen()函数让服务器进程进入监听状态 ④、调用accept()函数获取客户端的连接请求并建立连接 ⑤、调用read/recv、write/send 与客户端进行通信 ⑥、调用close()关闭套接字。 下面我们就根据上面列举的步骤来编写一个简答地服务器应用程序代码如下所示 本例程源码对应的路径为开发板光盘-11、Linux C 应用编程例程源码-30_socket-socket_server.c。
#include stdio.h
#include stdlib.h
#include unistd.h
#include string.h
#include sys/types.h
#include sys/socket.h
#include arpa/inet.h
#include netinet/in.h
#define SERVER_PORT 8888 // 端口号不能发生冲突,不常用的端口号通常大于5000
int main(void)
{struct sockaddr_in server_addr {0};struct sockaddr_in client_addr {0};char ip_str[20] {0};int sockfd, connfd;int addrlen sizeof(client_addr);char recvbuf[512];int ret;/* 打开套接字得到套接字描述符*/sockfd socket(AF_INET, SOCK_STREAM, 0);if (0 sockfd){perror(socket error);exit(EXIT_FAILURE);}/* 将套接字与指定端口号进行绑定*/server_addr.sin_family AF_INET;server_addr.sin_addr.s_addr htonl(INADDR_ANY);server_addr.sin_port htons(SERVER_PORT);ret bind(sockfd, (struct sockaddr *)server_addr, sizeof(server_addr));if (0 ret){perror(bind error);close(sockfd);exit(EXIT_FAILURE);}/* 使服务器进入监听状态*/ret listen(sockfd, 50);if (0 ret){perror(listen error);close(sockfd);exit(EXIT_FAILURE);}/* 阻塞等待客户端连接*/connfd accept(sockfd, (struct sockaddr *)client_addr, addrlen);if (0 connfd){perror(accept error);close(sockfd);exit(EXIT_FAILURE);}printf(有客户端接入...\n);inet_ntop(AF_INET, client_addr.sin_addr.s_addr, ip_str, sizeof(ip_str));printf(客户端主机的IP 地址: %s\n, ip_str);printf(客户端进程的端口号: %d\n, client_addr.sin_port);/* 接收客户端发送过来的数据*/for (;;){// 接收缓冲区清零memset(recvbuf, 0x0, sizeof(recvbuf));// 读数据ret recv(connfd, recvbuf, sizeof(recvbuf), 0);if (0 ret){perror(recv error);close(connfd);break;}// 将读取到的数据以字符串形式打印出来printf(from client: %s\n, recvbuf);// 如果读取到exit则关闭套接字退出程序if (0 strncmp(exit, recvbuf, 4)){printf(server exit...\n);close(connfd);break;}}/* 关闭套接字*/close(sockfd);exit(EXIT_SUCCESS);
}
以上我们实现了一个非常简单地服务器应用程序根据上面列举的步骤完成了这个示例代码最终的功能是当客户端连接到服务器之后客户端会向服务器也就是本程序发送数据在我们服务器应用程序中会读取客户端发送的数据并将其打印出来就是这么简单的一个功能。 SERVER_PORT 宏指定了本服务器绑定的端口号这里我们将端口号设置为8888端口不能与其它服务器的端口号发生冲突不常用的端口号通常大于5000。 代码就不再解释了都非常简单
编写客户端程序
接下来我们再编写一个简单地客户端应用程序客户端的功能是连接上小节所实现的服务器连接成功之后向服务器发送数据发送的数据由用户输入。示例代码如下所示 本例程源码对应的路径为开发板光盘-11、Linux C 应用编程例程源码-30_socket-socket_client.c。
#include stdio.h
#include stdlib.h
#include unistd.h
#include string.h
#include sys/types.h
#include sys/socket.h
#include arpa/inet.h
#include netinet/in.h
#define SERVER_PORT 8888 // 服务器的端口号
#define SERVER_IP 192.168.1.150 // 服务器的IP 地址
int main(void)
{struct sockaddr_in server_addr {0};char buf[512];int sockfd;int ret;/* 打开套接字得到套接字描述符*/sockfd socket(AF_INET, SOCK_STREAM, 0);if (0 sockfd){perror(socket error);exit(EXIT_FAILURE);}/* 调用connect 连接远端服务器*/server_addr.sin_family AF_INET;server_addr.sin_port htons(SERVER_PORT); // 端口号inet_pton(AF_INET, SERVER_IP, server_addr.sin_addr); // IP 地址ret connect(sockfd, (struct sockaddr *)server_addr, sizeof(server_addr));if (0 ret){perror(connect error);close(sockfd);exit(EXIT_FAILURE);}printf(服务器连接成功...\n\n);/* 向服务器发送数据*/for (;;){// 清理缓冲区memset(buf, 0x0, sizeof(buf));// 接收用户输入的字符串数据printf(Please enter a string: );fgets(buf, sizeof(buf), stdin);// 将用户输入的数据发送给服务器ret send(sockfd, buf, strlen(buf), 0);if (0 ret){perror(send error);break;}// 输入了exit退出循环if (0 strncmp(buf, exit, 4))break;}close(sockfd);exit(EXIT_SUCCESS);
}
代码不再说明需要注意的是SERVER_IP 和SERVER_PORT 指的是服务器的IP 地址和端口号服务器的IP 地址根据实际情况进行设置服务器应用程序示例代码30.4.1 中我们绑定的端口号为8888所以在客户端应用程序中我们也需要指定SERVER_PORT 为8888。 编译测试 这里笔者将服务器程序运行在开发板上而将客户端应用程序运行在Ubuntu 系统当然你也可以将客户端和服务器程序都运行在开发板或Ubuntu 系统这都是没问题的。 首先编译服务器应用程序和客户端应用程序
编译得到client 和server 可执行文件server 可执行文件在开发板上运行client 可执行文件在PC 端 Ubuntu 系统下运行。将server 可执行文件拷贝到开发板/home/root 目录下如下所示
在开发板执行server
接着在Ubuntu 系统执行客户端程序
客户端运行之后将会去连接远端服务器连接成功便会打印出信息“服务器连接成功…”此时服务器也会监测到客户端连接会打印相应的信息如下所示 接下来我们便可以在客户端处输入字符串客户端程序会将我们输入的字符串信息发送给服务器服务器接收到之后将其打印出来如下所示 总结 到此本章的内容就结束了内容讲得非常浅目的其实并不是让大家学会网络编程这个是不可能的旨在以引导大家入门为主让大家对socket 网络编程有一个基本的了解和认识。因为网络编程本就是应用编程中一门比较专业的方向如果大家将来想从事这方面的工作、或者以后从事了这方面工作再去找资料好好学习如果没有这个打算那就不要去深入研究这个有了基本的了解、认识就行了。