一、部分基础知识
简单讲一下基础知识,便于后面代码的理解,建议大概浏览一下这一小节内容。这里讲的只是冰山一角,建议大家学习计算机网络相关知识,推荐几本书:
- 《计算机网络》(谢希仁)
- 《计算机网络 自顶向下方法》
- 《计算机网络技术》
- 《计算机网络基础及应用》
- 《Linux C从入门到精通》
1.1 计算机网络的体系结构
1.11 互联网简介
互联网是一个巨大的网络系统,从工作方式上看,它由边缘部分和核心部分组成。
边缘部分是指连接到互联网的终端设备,如手机、电脑、路由器等;而核心部分则是指连接这些设备的网络基础设施。
-
边缘部分
- 边缘部分指代连接到互联网的终端用户设备,如台式电脑、笔记本电脑、智能手机、平板电脑等。这些设备都有一个共同的特点,就是它们都使用TCP/IP协议(准确说是基于TCP/IP协议簇的各种协议)来进行网络通信。这个协议有助于确保数据包从发送端到接收端的可靠性,并尽可能快地传输数据。
- 用户设备通过各种途径连接到互联网,例如通过宽带或移动网络。这些设备通过接入网络来访问各种网络应用程序,如电子邮件、社交媒体和在线购物等。边缘部分是互联网的主要组成部分,它为用户提供访问互联网的途径和服务。
- 边缘部分指代连接到互联网的终端用户设备,如台式电脑、笔记本电脑、智能手机、平板电脑等。这些设备都有一个共同的特点,就是它们都使用TCP/IP协议(准确说是基于TCP/IP协议簇的各种协议)来进行网络通信。这个协议有助于确保数据包从发送端到接收端的可靠性,并尽可能快地传输数据。
-
核心部分
- 核心部分是指连接边缘部分的基础网络设备,
如路由器、交换机、网关和服务器等
。这些设备通过全球互联网络将数据从一个地方传输到另一个地方。核心部分是互联网的支持结构,它确保在用户设备之间进行有效地网络交换。 - 核心部分采用了复杂的技术,如大规模的路由协议、数据流量控制和安全性等。这些技术确保所有数据在网络中的传输是快速、安全和可靠的。
- 核心部分是指连接边缘部分的基础网络设备,
从逻辑功能上可以划分为:资源子网和通信子网。
- 资源子网
- 资源子网由主机、终端、终端控制器、联网外设、各种软件资源与信息资源组成。资源子网负责全网的数据处理业务,并向网络用户提供各种网络资源与网络服务。连接到网络中的计算机、文件服务器以及软件构成了网络的资源子网。
- 通信子网
- 通信子网由网络通信控制处理机、通信线路与其他通信设备组成,完成全网数据传输、转发等通信处理工作。
1.12 计算机网络的分类
计算机网络有很多分类方式,至少需要了解一些名词得含义(都是耳熟能详的),比如局域网、专用网、无线网等等。
(1)按照网络的覆盖范围分类
-
广域网
WAN
(Wide Area Network):广域网的作用范围通常为几十到几千公里,因而有时也称为远程网(long haul network)。广域网是互联网的核心部分,其任务是通过长距离(例如,跨越不同的国家)运送主机所发送的数据。连接广域网各结点交换机的链路一般都是高速链路,具有较大的通信容量。 -
城域网
MAN
(Metropolitan Area Network):城域网的作用范围一般是一个城市,可跨越几个街区甚至整个城市,其作用距离约为5~50km。城域网可以为一个或几个单位所拥有,但也可以是一种公用设施,用来将多个局域网进行互连。目前很多城域网采用的是以太网技术,因此有时也常并入局域网的范围进行讨论。 -
局域网
LAN
(Local Area Network):局域网一般用微型计算机或工作站通过高速通信线路相连(速率通常在10Mbit/s以上),但地理上则局限在较小的范围(如1km左右)。在局域网发展的初期,一个学校或工厂往往只拥有一个局域网,但现在局域网已非常广泛地使用,学校或企业大都拥有许多个互连的局域网(这样的网络常称为校园网或企业网)。我们将在第3章3.3至3.5节详细讨论局域网。 -
个人区域网PAN(Personal Area Network)个人区域网就是在个人工作的地方把属于个人使用的电子设备(如便携式电脑等)用无线技术连接起来的网络,因此也常称为无线个人区域网WPAN(Wireless PAN),其范围很小,大约在10m左右。
若中央处理机之间的距离非常近(如仅1米的数量级或甚至更小些),则一般就称之为多处理机系统而不称它为计算机网络。
(2)按照使用者划分
- 公用网:公用网由电信部门组建,一般由政府电信部门管理和控制,网络内的传输和交换装置可提供(如租用)给任何部门和单位使用。公用网分为公共电话交换网(PSTN)、数字数据(DDN)、综合业务数字网(ISDN)等。
- 专用网:专用网是由某个单位或部门组建的,不允许其他部门或单位使用,例如金融、铁路等行业都有自己的专用网。专用网可以是租用电信部门的传输线路,也可以是自己铺设的线路,但后者的成本非常高。
(3)按照传输介质分
- 有线网:有线网是指采用双绞线、同轴电缆以及光纤作为传输介质的计算机网络。
- 无线网:无线网是指使用空间电磁波作为传输介质的计算机网络,它可以传送无线电波和卫星信号。无线网包括无线电话网、语音广播网、无线电视网、微波通信网、卫星通信网。
(4)按照交换技术分
- 电路交换网络:电路交换网络指在进行数据传输期间,发送点(源)与接收点(目的)之间构成一条实际连接的专用物理线路,最典型的电路交换网络就是公用电话交换网。
- 报文交换网络:报文交换又称为存储-转发技术,该方式不需要建立一条专用的物理线路,信息先被分解成报文,然后一站一站地从源头送达目的地,这有点类似通常的邮政寄信方式。
- 分组交换网络:分组交换网络的基本原理与报文交换相同,它也不需要建立专用的物理线路,但信息传送的单位不是报文而是分组,分组的最大长度比报文短得多。
1.13 协议与网络的分层体系结构
▶ 协议
共享计算机网络的资源,以及在网中交换信息,就需要实现不同系统中的实体的通信。实体包括用户应用程序、文件传输信息包、数据库管理系统、电子邮件设备以及终端等。两个实体要想成功地通信,它们必须具有同样的语言。交流什么,怎样交流以及何时交流,都必须遵从有关实体间某种相互都能接受的一些规则,这些规则的集合称为协议。协议的关键成分如下。
- 语法(Syntax)
语法确定协议元素的格式,即规定了数据与控制信息的结构和格式。 - 语义(Semantics)
语义确定协议元素的类型,即规定了通信双方要发出何种控制信息、完成何种动作以及做出何种应答。 - 定时(Timing)
定时可以确定通信速度的匹配和排序,即有关事件实现顺序的详细说明。
▶ 网络的分层体系结构
体系结构是研究系统各部分组成及相互关系的技术科学。
计算机网络体系结构采用分层配对结构,用于定义和描述一组用于计算机及其通信设施之间互联的标准和规范的集合。遵循这组规范可以很方便地实现计算机设备之间的通信。也就是说,为了完成计算机之间的通信合作,把每台计算机互联的功能划分成有明确定义的层次,并规定了同层次进程通信的协议及相邻层之间的接口及服务,这些同层进程通信的协议以及相邻层的接口统称为网络的体系结构。
为了减小计算机网络的复杂程度,按照结构化设计方法,计算机网络将其功能划分成若干个层次(Lyer),较高层次建立在较低层次的基础上,并为更高层次提供必要的服务功能。
(1)基本概念
- 实体:实体是通信时能发送和接收信息的任何软硬件设施。在网络分层体系结构中,每一层都由一些实体组成。
- 接口:分层结构中各相邻层之间要有一个接口,它定义了低层向其相邻的高层提供的原始操作和服务。相邻层通过它们之间的接口交换信息,高层并不需要知道低层是如何实现的,仅需要知道该层通过层间的接口所提供的服务,这样使得两层之间保持了功能的独立性。
(2)层次结构的特点
- 按照结构化设计方法,计算机网络将其功能划分为若干个层次,较高层次建立在较低层次的基础上,并为其更高层次提供必要的服务功能。
- 网络中的每一层都起到隔离作用,使得低层功能的具体实现方法的变更不会影响到高层所执行的功能。即低层对于高层而言是透明的。
(3)层次结构的优点
- 层之间相互独立。高层并不需要知道低层是如何实现的,而仅需要知道该层通过层间的接口所提供的服务。各层都可以采用最合适的技术来实现,各层实现技术的改变不影响其他层。
- 灵活性好。任何一层发生变化时,只要接口保持不变,则该层及其以下各层均不受影响。若某层提供的服务不再需要时,甚至可将这层取消。
- 易于实现和维护。整个系统已被分解为若干个易于处理的部分,这种结构使得一个庞大而又复杂的系统的实现和维护变得容易控制。
- 有利于网络标准化。因为每一层的功能和所提供的服务都已有了精确的说明,所以标准化变得较为容易。
1.14 OSI 七层模型(重要)
20 世纪 80 年代末和 90年代初,网络的规模和数量得到了迅猛的扩大和增长。但是许多网络都是基于不同的硬件和软件而实现的,这使得它们之间互不兼容。显然,在使用不同标准的网络之间是很难实现其通信的。为解决这个问题,国际标准化组织 ISO 研究了许多网络方案,认识到需要建立一种有助于网络的建设者们实现网络、并用于通信和协同工作的网络模型,因此在 1984年公布了开放式系统互连参考模型,称为 OSI/RM 参考模型(
Open System Interconnect Reference Model/Reference Model
),简称为OSI
参考模型。
▶ OSI 模型的结构
上图:
相关概念:
(1) 层:开放系统的逻辑划分,代表功能上相对独立的一个子系统。
(2) 对等层:指不同开放系统的相同层次。
(3) 层功能:本层具有的通信能力,它由标准来指定。
(4) 层服务:本层向相邻高层提供的通信能力。根据 OSI 增值服务的原则,本层服务应是其所有下层服务与本层功能之和。
▶ OSI 模型各层的功能
上图:
不展开讲了,本文是介绍socket的😅
1.15 TCP/IP 的体系结构(重要)
OSI 参考模型具有定义过于繁杂、实现困难等缺陷。与此同时,TCP/IP 协议的出现和广泛使用,特别是因特网用户爆炸式的增长,使TCP/IP 网络的体系结构日益显示出其重要性。
TCP/IP 是指传输控制协议/网际协议,它是由多个独立定义的协议组合在一起的协议集合。TCP/IP 协议是目前最流行的商业化网络协议,尽管它不是某一标准化组织提出的正式标准,但它已经被公认为目前的工业标准或“事实标准”。因特网之所以能迅速发展,就是因为TCP/IP 协议能够适应和满足世界范围内数据通信的需要。
TCP/IP协议的特点
(1) 开放的协议标准,可以免费使用,并且独立于特定的计算机硬件与操作系统。
(2) 独立于特定的网络硬件,可以运行在局域网、广域网以及因特网中。
(3) 统一的网络地址分配方案,使得整个 TCP/IP 设备在网络中都具有唯一的地址。
(4) 标准化的高层协议,可以提供多种可靠的用户服务。
TCP/IP 体系结构将网络划分为 4 层,它们分别是应用层(Application layer)、传输层(Transport layer)、网际层(Internet layer)和网络接口层(主机-网络层)(Network interface layer),与OSI模型的对应关系如下:
▶ TCP/IP 体系结构各层的功能
-
网络接口层
在 TCP/IP 分层体系结构中,网络接口层又称主机-网络层,它是最低层,负责接收网际层的 IP 数据报以形成帧发送到传输介质上;或者从网络上接收帧,抽取数据报交给互连层。它包括了能使用 TCP/IP 与物理网络进行通信的所有协议。TCP/IP 体系结构并未定义具体的网络接口层协议,旨在提高灵活性,以适应各种网络类型,如 LAN、WAN。它允许主机连入网络时使用多种现成的和流行的协议,例如局域网协
议或其他一些协议。 -
网际层
网际层又称互连层,是 TCP/IP 体系结构的第二层,它实现的功能相当于 OSI 参考模型中网络层的功能。网际层的主要功能如下。- 处理来自传输层的分组发送请求。在收到分组发送请求之后,将分组装入 IP 数据报,填充报头,选择发送路径,然后将数据报发送到相应的网络接口。
- 处理接收的数据报。检查收到的数据报的合法性,进行路由。在接收到其他主机发送的数据报之后,检查目的地址,如需要转发,则选择发送路径,转发出去;如目的地址为本节点 IP 地址,则除去报头,将分组送交传输层处理。
- 处理 ICMP 报文、路由、流控与拥塞问题。
-
传输层
传输层位于网际层之上,它的主要功能是负责应用进程之间的端到端通信。在 TCP/IP体系结构中,设计传输层的主要目的是在互连层中的源主机与目的主机的对等实体之间建立用于会话的端到端连接。因此,它与 OSI 参考模型的传输层相似。 -
应用层
应用层是最高层。它与 OSI 模型中的高 3 层的任务相同,都是用于提供网络服务,比如文件传输、远程登录、域名服务和简单网络管理等。
▶ TCP/IP协议簇
TCP/IP中各层的协议如图 :
我们每天都在使用近乎所有的协议。
这些协议都很有用,不展开了,下面介绍一下TCP和UDP协议。
1.2 本文使用的主要协议 (必备)
前面的知识点或许你浏览一下就可以了,在下面这些知识点是socket编程中必备知识点。
1.21 Mac地址、IP地址与端口号
网络地址就是网络中唯一标识网络中每台网络设备的一个数字,若没有这种唯一的地址,网络中的计算机之间就不可能进行可靠的通信。实际上网络中每个节点都有两类地址标识:数据链路层地址和网络层地址。
▶ Mac地址
网络上的每一个设备有一个唯一的物理地址(Physical Address),有时被称为硬件地址或数据链路地址。数据链路层地址是与网络硬件相关联的固定序列号,通常在出厂前即被确定(也可以通过一些方式手动修改,某些情况下还会出现mac地址冲突的情况,不过很少)。
这些地址通过位于数据链路层中的介质访问控制(MAC,Media Access Control
)子层后被称为 MAC 地址。它是在媒体接入层上使用的地址,由网络设备制造商生产时写在硬件内部。MAC 地址与网络无关,无论将带有这个地址的硬件(如网卡、路由器等)接入到网络的何处,该硬件都有相同的 MAC 地址。(可以把他比喻为你的身份证号,一出生就确定,无论你走到哪里,它都不变)
对于网络硬件而言,地址通常被编码到网络的接口卡中。常见的情况是,用户根本不能改变这些地址,因为这个唯一的编号已经编到可编程只读存储器(PROM)中。
例如以太网卡的 MAC 地址由厂商写在网卡的 BIOS 里,为 6 字节 48 比特的 MAC 地址。这个 48比特都有其规定的意义,前 24 位是由 IEEE(电气与电子工程师协会)分配,称为机构唯一标识符(OUI,OrganizationllyUnity Idientifier);后 24 位由厂商自行分配,这样的分配使得世界上任意一个拥有 48 位 MAC地址的网卡都有唯一的标识。
以太网卡的MAC地址通常表示为12个十六进制数,每两个十六进制数之间用冒号隔开,如 08:00:20:0A:8C:6D
就是一个 MAC 地址,其中前 6 位十六进制数 08:00:20 代表网络硬件制造商的编号,它由 IEEE 分配,而后 6 位十六进制数 0A:8C:6D 代表该制造商所制造的某个网络产品(如网卡)的系列号。每个网络制造商必须确保它所制造的每个网络设备都具有相同的前 3 字节以及不同的后 3 个字节。这样就可保证世界上每个设备都具有唯一的 MAC 地址。
通信过程中需要有两个地址:一个地址标识发送设备(源);一个用于接收设备(目的)。数据链路层的 PDU 包含了目的 MAC 地址和源 MAC 地址,它是确认通信双方身份的唯一标识。通过 MAC 地址的识别,才能准确、可靠地找到对方,也才能够实现通信。MAC 地址用于标识本地网络上的系统。大多数数据链路层协议,包括以太网和令牌环网协议,都使用制造商通过硬编码写入网卡的地址。
我们可以使用查看电脑的网卡的mac地址,比如Linux使用:ifconfig -a
ether 52:57:00:4b:ac:85 txqueuelen 1000 (Ethernet)
▶ IP地址
网络地址是逻辑地址,该地址可以通过操作系统进行定义和更改。网络地址采用一种分层编址方案,如同个人通信地址包括国家、省、市、街道、住宅号及个人姓名一样,网络分类逻辑化,越容易管理和使用,因而更加有用。(你可能在一个地方住一段时间,这段时间内,你的通信地址不变;当你换个地方住,出差、旅游的时候,你的地址又会发生改变)
在 TCP/IP 环境中,每个节点都具有唯一的 IP 地址。每个网络被看作一个单独的、唯一的地址。在访问到这个网络内的主机之前,必须首先访问到这个网络。
◐ IP地址的表示方法
TCP/IP 协议栈中的 IP(IPv4)地址是网络地址,为标识主机而采用 32 位无符号二进制数表示。
为了方便用户的理解和记忆,它采用了点分十进制标记法,即将 4 字节的二进制数值转换成 4 个十进制数值,每个数值小于等于 255,数值中间用“.”隔开,表示成为 w.x.y.z 的形式,因此,最小的 IPv4 地址值为 0.0.0.0,最大的地址值为 255.255.255.255,
举例:
同样你可以使用ipconfig
或ifconfig
查看电脑ip地址。
◐ IP地址的分类
IP地址的编址方式经历了3个阶段:
- 分类的IP地址
- 划分子网
- CIDR
(1)2级 IP地址
过去,将一个ip地址可以分为两部分,网络号和主机号,即IP 地址由网络号(Net id)和主机号(Host id)两个层次组成。
网络号用来标识互联网中的一个特定网络,而主机号则用来表示该网络中主机的一个特定连接。因此,IP 地址的编址方式明显携带了位置信息。这给 IP 互联网的路由选择带来了很大好处。
TCP/IP 规定,只有同一网络(网络号相同)内的主机才能够直接通信,不同网络内的主机,只有通过其他网络设备(如路由器),才能够进行通信。
在长度为 32 位的 IP 地址中,哪些位代表网络号,哪些代表主机号呢?
这个问题看似简单,意义却非常重大,只有明确其网络号和主机号,才能确定其通信地址;同时当地址长度确定后,网络号长度又将决定整个互联网中可以包含多少个网络,主机号长度则决定每个网络能容纳多少台主机。
为了适应各种网络规模的不同,IP 协议将 IP 地址划分为 5 类网络(A、B、C、D 和 E),它们分别使用 IP 地址的前几位(地址类别)加以区分,常用的为 A、B 和 C 三类。
A、B、C类IP地址可以容纳的网络数和主机数:
(2)划分子网和子网掩码
随着网络设备的爆发增长,前面的ip地址划分方式表现出很大的缺陷:
- 地址空间利用率低;
- 不灵活;
- 路由表会很大
从 1985 年起,IP 地址中增加了一个“子网号字段”,使两级的 IP 地址变成三级的 IP 地址,也就是 IP 地址由网络号、子网号和主机号3 部分组成。
划分子网只是从 IP 地址的主机号中拿出几位来作为子网号,而不改变 IP 地址的网络号字段,即:
子网划分规则:
(1) 子网化的规则不允许使用全 0 或者全 1 的子网地址,这些地址是保留的。因此只有 1 位数时,不能得到可用的子网地址。
(2)在利用主机号划分子网后,剩余的主机号部分,全部为“0”的表示该子网的网络号,全部为“1”的则表示该子网的广播地址,剩余的就可以作为主机号分配给子网中的主机。也就是说,剩余的主机号部分的二进制全“0”或全“1”的子网号不能分配给实际的子网。
机器是如何知道 IP 地址中哪些位数用来表示网络、子网和主机部分呢?
为了解决这个问题,子网编址使用了子网掩码(或称为子网屏蔽码)。子网掩码也采用了 32 位二进制数值,分别与 IP 地址的 32 位二进制数相对应。
IP 协议规定,在子网掩码中,与 IP 地址的网络号和子网号部分相对应的位使用“1”来表示,而与 IP 地址的主机号部分相对应的位则用“0”表示。将一台主机的 IP 地址和它的子网掩码按位进行“与”运算,就可以判断出 IP 地址中哪些位用来表示网络和子网,哪些位用来表示主机号。
图示:
(3)无分类编址(CIDR)
划分子网在一定程度上缓解了互联网在发展中遇到的困难。然而在1992年互联网仍然面临三个必须尽早解决的问题,这就是:
- B类地址在1992年已分配了近一半,眼看很快就将全部分配完毕!
- 互联网主干网上的路由表中的项目数急剧增长(从几千个增长到几万个)。
- 整个Pv4的地址空间最终将全部耗尽。在2011年2月3日,IANA宣布Pv4地址已经耗尽了。
早在1987年,RFC1009就指明了在一个划分子网的网络中可同时使用几个不同的子网掩码。使用变长子网掩码VLSM(Variable Length Subnet Mask)可进一步提高IP地址资源的利用率。在VLSM的基础上又进一步研究出无分类编址方法,它的正式名字是无分类域间路由选择CIDR
(Classless Inter–Domain Routing)。
CIDR 是传统地址分配策略的重大突破,它完全抛弃了有分类地址,前面介绍的有类地址用 8 位表示一个 A 类网络号,16 位表示一个 B 类网络号,24 位表示一个 C 类网络号。CIDR用网络前缀代替了这些类,前缀可以任意长度,而不仅仅是 8 位,16 位或 24 位。允许 CIDR可以根据网络大小来分配网络地址空间,而不是在预定义的网络地址空间中作裁剪。每一个CIDR 网络地址和一个相关位的掩码一起广播,这个掩码识别了网络前缀的长度。也就是说,一个网络地址中主机部分与网络部分的划分完全是由子网掩码确定的。
例如,使用192.125.61.8/20
标识一个 CIDR 地址,此地址有 20 位网络地址
◐ 特殊IP地址
(1) 网络地址
在互联网中,经常需要使用网络地址,那么,怎么来表示一个网络地址呢?IP 地址方案规定,一个网络地址包含了一个有效的网络号和一个全“0”的主机号。
例如,地址 113.0.0.0 就表示该网络是一个 A 类网络的网络地址。而一个 IP 地址为202.100.100.2的主机所处的网络地址为 202.100.100.0,它是一个 C 类网络,其主机号为 2。
(2) 广播地址
当一个设备向网络上所有的设备发送数据时,就产生了广播。为了使网络上所有设备能够注意到这样一个广播,必须使用一个可识别和侦听的 IP 地址。通常,一个广播的标志是,其目的 IP 地址的主机号是全“1”。IP 广播有两种形式,一种叫直接广播,另一种叫有限广播。
- ① 直接广播
如果广播地址包含一个有效的网络号和一个全“1”的主机号,则称之为直接广播(Directed Broadcasting)地址。在 IP 互联网中,任意一台主机均可向其他网络进行直接广播。
例如 C 类地址 202.100.100.255 就是一个直接广播地址。互联网上的一台主机如果使用该 IP 地址作为数据报的目的 IP地址,那么这个数据报将同时发送到 202.100.100.0 网络上的所有主机。
显然,直接广播的一个主要问题是在发送前必须知道目的网络的网络号。
- ② 有限广播
32 位数全为“1”的 IP 地址(255.255.255.255)用于本网广播,该地址称为有限广播(Limited Broadcasting)地址。实际上,有限广播将广播限制在最小的范围内。如果采用标准的 IP 编址,那么有限广播将被限制在本网络之中;如果采用子网编址,那么有限广播将被限
制在本子网之中。有限广播不需要知道网络号。因此,在主机不知道本机所处的网络时(如主机的启动过程中),只能采用有限广播方式。
(3) 回送地址(环回地址)
A 类网络地址 127.0.0.0 是一个保留地址,用于网络软件测试以及本地机器进程间通信,这个 IP 地址叫做回送地址(Loop back address)。无论什么程序,一旦使用回送地址发送数据,协议软件不进行任何网络传输,立即将之返回。因此,含有目的网络号 127 的数据报不可能出现在任何网络上。(我们常用的是127.0.0.1,称之为localhost,当然,你使用127.0.0.2等等也是可以的)
▶ 端口号
我们的电脑、移动设备一般只有1个ipv4地址,但通常由许多软件,只通过一个IP地址,怎么区分接收到的数据是谁的,发送的数据是谁发的呢?
——使用端口号。
端口号是用于在计算机网络中标识特定应用程序或服务的数字。它可以看作是一种与IP地址相结合的地址扩展,用于将网络流量正确地发送到目标应用程序。
在计算机网络通信中,每个网络连接都使用一个唯一的端口号来区分不同的应用程序或服务。端口号范围从0到65535
(为什么呢?因为IP、TCP等协议使用16位表示端口号),其中0到1023是称为"知名端口"的预留端口,用于一些常见的服务如HTTP(端口80)、FTP(端口21)、SSH(端口22)等。
通过将数据包的目的端口号和源端口号与IP地址结合使用,网络中的设备可以将数据正确地路由到目标应用程序或服务。端口号是网络通信中重要的组成部分,允许多个应用程序同时在同一设备上进行通信,每个应用程序都有唯一的标识符。
端口号主要用于标识网络通信中的应用程序或服务,而不是直接用于区分协议。然而,某些端口号通常与特定的协议相关联,因为特定的协议通常在预定的端口上进行通信。
例如,HTTP(超文本传输协议)通常使用端口号80,HTTPS(安全的超文本传输协议)通常使用端口号443,FTP(文件传输协议)通常使用端口号21,SSH(安全外壳协议)通常使用端口号22等。
因此,端口号经常与特定的协议相关联,以便网络设备和应用程序可以识别并将数据正确地传送到相应的服务或应用程序。然而,并非所有的端口号都与特定协议相关,因为在一些情况下,用户可以自定义端口号来与其特定应用程序关联。
▶ ipv6
ipv4地址早就分配完了。作为新一代的 Internet 的地址协议标准,它克服了 IPv4 的一些问题,但由于 IPv6 和 IPv4 协议不兼容,而现在 Internet 上的设备大多只支持 IPv4 协议,考虑到代价,不可能立即用 IPv6 代替 IPv4,所以目前一些网络设备都支持这 2 种协议,由用户来决定用什么协议。长远规划,IPv6 会代替 IPv4,因为 IPv6 有下列 IPv4 不具有的优势:庞大的地址空间、简化的报头定长结构、更合理的分段方法、完善的服务种类。
ipv6有很多特性,不讲了,比如不用DHCP分配,我的服务器就有3个ipv6地址。
现在很多网站、设别、软件、协议都开始很好地支持ipv6协议了。
▶ 域名
ip地址很有用,但我记不住啊。
虽然 IP 地址是 TCP/IP 的基础,但每个用过互联网的人都知道用户并不必记住或输入IP 地址。类似地,计算机也被赋予符号名字,当需要指定一台计算机时,应用软件允许用户输入这个符号名字。例如,在说明一个电子邮件的目的地时,用户输入一个字符串来标识接收者以及接收者的计算机。类似地,用户在输入字符串指定 WWW 上的站点时,计算机名字是嵌入在该字符串中的。
由于二进制形式的 IP地址比符号名字更为紧凑,在操作时需要的计算量更少。而且地址比名字占用更少的内存,在网络上传输需要的时间也更少。于是,尽管应用软件允许用户输入符号名字,基本网络协议仍要求使用地址——应用在使用每个名字进行通信前必须将它翻译成对等的 IP 地址。在大多数情况下,翻译是自动进行的,翻译结果对用户隐蔽——IP 地址保存在内存中,仅在收发数据报的时候使用。
把域名翻译成 IP 地址的软件称为域名系统(Domain Name System,DNS
)。例如,电子工业出版社的域名是:www.phei.com.cn。可以看出域名是有层次的,域名中最重要的部分位于右边。域可以继续划分为子域,如二级域、三级域等。域名的结构是由若干分量组成的,各分量之间用点隔开:….三级域名.二级域名.顶级域名。
每一个域名服务器(name server
)不但能够进行一些域名到 IP 地址的转换(这种转换常被称为地址解析),而且还必须具有连向其他域名服务器的信息,当自己不能进行域名到 IP 地址的转换时,就应该知道到什么地方去找别的域名服务器。互联网上的域名服务器系统也是按照域名的层次来安排的。每一个域名服务器都只对域名体系中的一部分进行管辖。
点击一个 URL 后,涉及的全过程可以大致描述如下:
解析 URL:浏览器首先会解析 URL(统一资源定位符),包括协议类型(如HTTP、HTTPS)、主机名(域名或IP地址)、端口号(可选)、路径等。
DNS 解析:如果主机名在本地 DNS 缓存中找不到,浏览器会向 DNS(域名系统)服务器发送请求,以获取与主机名对应的 IP 地址。
建立 TCP 连接:使用解析得到的 IP 地址和端口号,浏览器会尝试与目标服务器建立 TCP(传输控制协议)连接。这是一个三次握手的过程,用于建立可靠的数据传输通道。
发起 HTTP 请求:一旦建立了 TCP 连接,浏览器会向服务器发送 HTTP(超文本传输协议)请求,包括请求方法(如GET、POST)、请求头、请求体等。请求的目标是服务器上的特定资源(如网页、图像、API
等)。服务器处理请求:服务器接收到请求后,会根据请求的内容和服务器端的配置来处理请求。这可能涉及动态生成内容、从数据库检索数据、执行业务逻辑等操作。
返回 HTTP 响应:服务器处理完请求后,会生成一个 HTTP 响应,包括状态码、响应头、响应体等。状态码表示请求的结果,如200表示成功、404表示资源未找到等。
接收响应:浏览器接收到服务器返回的响应后,会开始解析响应内容。
渲染页面:如果响应的内容是一个 HTML 页面,浏览器会解析 HTML、加载和解析 CSS 和 JavaScript 文件,并根据标记、样式和脚本来构建页面的渲染树。最终,将渲染树转换为屏幕上的可视化布局和呈现。
完成请求:浏览器执行完页面的渲染后,触发相应的事件,可能会执行后续的 JavaScript 代码或处理其他交互。
又说多了,这篇文章写不完了。😅
1.22 TCP/UDP 协议
▶ Intro
TCP/IP 体系结构的传输层定义了传输控制协议(TCP
,Transport Control Protocol)和用户数据报协议(UDP
,User Datagram Protocol)两种协议。它们利用 IP 层提供的服务,分别提供端到端可靠的和不可靠的服务。应用层协议通常都是基于他们的。
TCP
:
- TCP 是一种面向连接的协议,提供可靠的、有序的、基于字节流的数据传输。
- 在使用 TCP 时,通信双方必须先建立连接,然后才能进行数据的传输。
- TCP 使用三次握手来建立连接,并使用四次挥手来关闭连接。
- TCP 提供可靠性,通过使用序列号、确认应答、超时重传、拥塞控制等机制来确保数据的可靠传输。
- TCP 支持点对点的通信方式,适用于需要可靠传输、有序交付和流控制的应用,如网页浏览、文件传输、电子邮件等。
UDP
:
- UDP 是一种无连接的协议,提供不可靠的、无序的、尽力而为的数据传输。
- 在使用 UDP 时,通信双方之间没有建立连接的过程,可以直接发送数据包。
- UDP 不提供可靠性保证,数据包可能会丢失、重复或者无序到达。
- UDP 以数据报(Datagram)的形式发送和接收数据,每个数据报都是独立的、完整的数据单元。
- UDP 没有拥塞控制和流量控制机制,可以实现更低的延迟和更高的传输速度。
- UDP 适用于对实时性要求较高的应用,如实时音视频传输、在线游戏等,也常用于 DNS 解析和简单的请求-响应通信。
关于他们的报文帧格式、连接建立过程、流量控制等,此处先不介绍。
哎,不行,报文格式还是必须说一下的。
▶ 报文首部格式、长度
应用层的进程发送数据,是将数据依次向下传递给运输层、网络层等等,最后通过物理传输到达目的地,没向下传输一层,都要在数据前面添加相应层的首部,首都通常用来指明数据部分的长度、使用的协议版本、校验和等等信息,使得数据可以在各层进行正确的交付。
此外,每一层的报文长度都会受到一些限制,有协议本身的、也有设备限制,因此,是将数据全部塞进一个报文传输,还是每次只传输特定长度的数据呢?每次传多长好呢?
◐ UDP报文首部格式、长度
报文=首部+数据
数据就不用说了奥,来看看首部。
这是UDP的首部格式(不看位首部,那个是计算校验和用的):
UDP报文的首部很短,只有8字节。注意长度这个字段,表示整个UDP数据报的长度(单位:字节),占2个字节理论来说最大长度可以是: 2 16 2^{16} 216 字节,但数据部分往往远远达不到 2 16 − 8 2^{16}-8 216−8个字节。
它还受:
- 以太网(Ethernet)数据帧的长度,数据链路层的MTU(最大传输单元)。
- socket的UDP发送缓存区大小。
- 设备限制。
- 等等。
的限制,通常,数据部分应该小于:548字节。
具体的计算可以参考这篇文章:UDP传输报文大小详解
◐ TCP报文首部格式、长度
、
TCP传输不像UPD那样,它提供可靠的、面向连接的字节流传输,因此TCP报文首部比较复杂:
按照上图,1个TCP报文段的最大长度为65495字节,TCP封装在IP内,IP数据报最大长度65535 ,头部最小20字节;TCP头部长度最小20字节,所以最大封装数据长度为65535-20-20=65495字节,实际情况下,还受很多因素影响,会短很多。
TCP 首部字段释义:
- 端口号:用来标识一台主机的不同进程
- 1) 源端端口号:源端口和IP层解析出来的IP地址标识报文的发送地,同时也确定了报文的返回地址;
- 2)目的端口号:表明了该数据报是发送给接收方计算机的具体的一个应用程序 。
- 序号和确定号:TCP可靠传输的保障
- 1) 序号:文段发送的数据组的第一个字节的序号。在TCP传送的流中,每一个字节一个序号。例如:一个报文段的序号为300,此报文段数据部分共有100字节,则下一个报文段的序号为400。所以序号确保了TCP传输的有序性
- 2)确认号:即
ACK
,指明下一个期待收到的字节序号,表明该序号之前的所有数据已经正确无误的收到。确认号只有当ACK标志为1时才有效。比如收到一个报文段的序号为300,报文段数据部分共有100字节,回复的ACK的确认序号就是400- 数据偏移:也叫做首部长度,以32位比特位为长度单位
- 使用4个比特位,因为报头数据中含有可选项字段,所以TCP报头的长度是不确定的,除去可选项字段TCP的长度为20字节,4bit最大表示的数据为15,15* (32 / 8)= 60 ,故。TCP报头最大长度为60字节
- 保留:为将来定义新的用途保留,现在一般置0
- 标志位:UGR ACK PSH RST SYN FIN ,六个标志位代表六个不同的功能
- 1) UGR: 紧急指针标志,为 1 时表示紧急指针有效,为 0 则忽略紧急指针
- 2) ACK: 确认序号标志,为 1 时表示确认序号有效,为 0 则表示报文中不含有确认信息,忽略确认字段号
- 3) PSH:push标志,为 1 表示是带有push标志的数据,指示接收方在接收到数据以后,应尽快的将这个报文交付到应用程序,而不是在缓冲区缓冲
- 4)RST: 重置连接标志,用于由主机崩溃或者其他原因而出现错误的连接,或者用于拒绝非法的报文段和拒绝连接请求
- 5)
SYN
: 同步序号,用于连接建立过程,在请求连接的时候,SYN=1和ACK=0表示该数据段没有捎带确认域,而连接应答捎带一个确认,表示为SYN=1和ACK=1- 6)
FIN
: 断开连接标志,用于释放连接,为 1 表示发送方已经没有数据发送,即关闭数据流- 窗口:滑动窗口大小,用来告知发送端接收端的缓存大小,以此来控制发送端的发送速率,从而达到流量控制。窗口大小是一个16比特位的字段,因而窗口大小最大为65535字节
- 校验和: 奇偶校验,此校验和是对整个TCP报文段,包括TCP头部和TCP数据,以16位字进行计算所得,由发送端计算和存储,并由接收端进行验证
- 紧急指针: 只有当URG标志置为1的时候,紧急指针才有效,紧急指针是一个正的偏移量,和顺序号字段中的值相加表示紧急数据最后一个字节的序号。TCP的紧急方式是发送端向另一端发送紧急数据的一种方式
- 选项和填充: 最常见的可选字段是最长报文的大小,又称为MSS,每个连接方通常在通信的第一个报文段(也就是第一次握手的SYN报文的时候)中指明这个选项,表示本端所能接受的最大报文段的长度。提示:选项长度不一定是32位的整倍数,所以要有填充位,即在这个字段中加入额外的零,以保证TCP头是32的整倍数
- 数据部分:TCP报文段中的数据部分是可选的,在一个建立连接和断开连接的时候,双方交换的报文段只有TCP的首部。如果一方没有数据要发送,也灭幼使用任何数据的首部俩确认收到的数据,在处理超时的许多情况中,也会发送不带你任何数据的报文段。
1.3套接字编程
1.31 套接字名称分类
套接字用于在2个进程之间建立连接,就像一个套接管一样。
套接管(灵魂画手😁):
虽然说这名字挺有有道理,但真不好理解,很多人连套接管都不知道。我更喜欢叫他原名socket
(它的翻译时插座、插口)。
socket用来唯一标识网络中的一个通信连接,套接字=ip地址:端口号,如10.0.0.1:443
但是下面这些,也可以称为socket:
- 允许应用程序访问连网协议的应用编程接口 API (Application ProgrammingInterface),即运输层和应用层之间的一种接口,称为
socket API
,并简称为socket。 - 在socket API中使用的一个函数名也叫做socket.
- 调用socket函数的端点称为socket,.如“创建一个数据报socket”。
- 调用socket函数时,其返回值称为socket描述符,可简称为socket.
- 在操作系统内核中连网协议的Berkeley实现,称为socket实现。
3和4,也可以称为:句柄。
句柄:(比如文件句柄、窗口句柄、内存句柄、对象句柄等等)也叫做描述符,它通常是一个指针,指向一个描述某个对象的数据结构,这个数据结构可以使对象的属性、方法、状态或者数据等等。句柄是一种抽象和封装,隐藏了许多底层的细节。(通常Windows下叫句柄,Linux下叫描述符)
套接字分为:原始套接字、流式套接字、数据报套接字:
- 原始套接字使开发人员能对底层的数据传输进行控制,原始套接字下接收的数据含有IP首部;
- 流式套接字(stream socket)提供双向、有序、可靠的数据服务,通信前需要先建立连接,TCP采用的就是流式套接字,因此,我们通常称为TCP套接字。
- 数据报套接字:不能保证可靠、有序和不重复,也称为UDP套接字。
下面先从简单的UDP套接字开始介绍。
1.32 socket编程使用的函数、常量、结构体
各种编程语言、操作系统,都有相应的socket编程API,他们的函数、数据结构可能会有一些差异,但功能都是类似的。
下面是socket编程中常用的内容:
函数:
-
socket()
: 创建一个套接字 -
bind()
: 将套接字与特定的地址和端口绑定 -
listen()
: 监听传入的连接请求 -
accept()
: 接受连接请求,创建一个新的套接字用于通信 -
connect()
: 建立与远程套接字的连接 -
send()/sendto():
发送数据 -
recv()/recvfrom()
: 接收数据 -
close()
: 关闭套接字
常量:
-
AF_INET
: IPv4地址族 -
AF_INET6:
IPv6地址族 -
SOCK_STREAM
: 流式套接字,通常用于TCP -
SOCK_DGRAM
: 数据报套接字,通常用于UDP -
IPPROTO_TCP
: TCP协议 -
IPPROTO_UDP
: UDP协议
结构体:
- sockaddr_in: IPv4地址结构
- sockaddr_in6: IPv6地址结构
- sockaddr: 通用地址结构,用于在函数中表示地址
这只是一些常见的函数、常量和结构体示例,实际上可能还有其他函数和相关的数据结构和常量,具体取决于编程语言和操作系统的支持。
后面还会具体介绍。
1.33 UDP套接字
▶ UDP套接字简介
-
UDP套接字概述
UDP是一种面向数据报的传输协议,而UDP套接字则提供了对UDP协议的抽象接口。UDP套接字通过数据报进行通信,每个数据报是一个独立的、不可拆分的消息单元。UDP套接字以无连接的方式进行通信,不需要在发送和接收数据之前建立连接。 -
特点与优势
- 无连接性:UDP套接字不需要在通信之前建立连接,通信双方可以直接发送和接收数据报。这使得UDP套接字的建立和断开的开销较低,适用于一对多的广播或多播通信。
- 不可靠性:UDP协议本身不提供数据的可靠传输机制,数据报可能会丢失、重复、乱序或损坏。这使得UDP套接字适用于那些对数据传输的实时性要求较高,但可靠性要求较低的应用,如实时音视频传输。
- 低延迟:由于无连接的特性,UDP套接字具有较低的传输延迟。它避免了TCP的连接建立和断开过程,适合于需要快速传输数据、对准确性要求不严格的场景。
-
应用场景
UDP套接字在以下场景中得到广泛应用:- 实时音视频传输:UDP套接字适用于实时音视频传输,如视频会议、实时直播等。虽然数据报可能会丢失,但可以通过其他机制进行丢失恢复或补偿。
- 游戏应用:多人在线游戏通常需要快速传输玩家的位置和动作信息,UDP套接字提供了低延迟和即时性的数据传输,使得游戏体验更加流畅。
- 广播和多播:UDP套接字可以进行一对多的广播和多播通信。它可以将数据报发送到多个目标地址,适用于需要同时向多个客户端发送相同数据的场景,如广播消息、设备发现等。
-
与TCP套接字的区别
UDP套接字与TCP套接字在特性上存在明显的区别:- 连接性:TCP套接字是面向连接的,需要在通信之前建立连接。而UDP套接字是无连接的,通信双方可以直接发送和接收数据报。
- 可靠性:TCP套接字提供可靠的数据传输,通过确认和重传机制来确保数据的完整性和顺序性。UDP套接字则不提供数据的可靠传输,数据报可能会丢失、重复、乱序或损坏。
- 有序性:TCP套接字保证数据的有序传输,而UDP套接字不保证数据报的顺序,不同的数据报可能以不同的顺序到达目标。
TCP、UDP可以在很多模式下工作(对等、多播、广播),本文主要介绍经典的服务器-客户端模式(C/S),这也是实际应用中最常见的工作模式,下面的通信过程、代码,默认试试C/S模式。
▶ UDP套接字通信过程
当使用UDP套接字进行通信时,过程比较简单,以下是UDP套接字的通信过程的详细说明:
创建套接字:使用
socket()
函数创建一个UDP套接字。指定协议簇(如AF_INET)和套接字类型(如SOCK_DGRAM)。绑定套接字:使用
bind()
函数将套接字绑定到本地地址和端口。绑定套接字可以让操作系统知道该套接字要使用的本地地址和端口号。接收数据:调用
recvfrom()
函数等待接收来自网络中其他主机的UDP数据报。该函数会阻塞当前进程,直到有数据到达套接字。当数据到达时,操作系统将数据复制到应用程序指定的接收缓冲区,并返回数据的发送者的地址和端口。处理数据:应用程序可以对接收到的UDP数据报进行处理。这可能包括解析数据报的内容、验证数据的完整性、进行数据处理等操作。
准备发送数据:准备要发送的UDP数据报,包括目标地址和端口号以及要发送的数据内容。
发送数据:使用
sendto()
函数将UDP数据报发送给特定的目标地址和端口。可以指定目标地址为其他主机的IP地址和端口号。操作系统将数据报发送到网络中,并不关心是否成功到达目标主机。等待响应(可选):应用程序可以选择等待接收来自目标主机的响应数据。这需要调用recvfrom()函数等待接收响应数据报。
关闭套接字:使用
close()
函数关闭UDP套接字,释放相关的资源。
1.34 TCP套接字
就不继续啰嗦介绍特点什么的了,前面有对比。
▶ TCP套接字通信过程
TCP 需要先建立连接,才传输数据。
创建套接字:使用
socket()
函数创建一个TCP套接字。指定协议簇(如AF_INET)和套接字类型(如SOCK_STREAM)。绑定套接字(可选):如果是服务器端,可以使用
bind()
函数将套接字绑定到指定的本地地址和端口。这样客户端就可以连接到该地址和端口进行通信。如果是客户端,可以省略此步骤。监听连接(仅服务器端):如果是服务器端,使用
listen()
函数开始监听来自客户端的连接请求。指定同时允许多少个连接请求进入待处理队列。建立连接(仅客户端):如果是客户端,使用
connect()
函数连接到服务器端的指定地址和端口。该函数会阻塞当前进程,直到连接成功建立或发生错误。接受连接请求(仅服务器端):使用
accept()
函数接受客户端的连接请求。该函数会阻塞当前进程,直到有客户端连接进入。一旦连接被接受,将创建一个新的套接字来处理与该客户端的通信。发送数据:使用
send()
函数发送数据给连接的对方。可以将要发送的数据放入发送缓冲区。接收数据:使用
recv()
函数等待接收来自对方的数据。该函数会阻塞当前进程,直到有数据到达。一旦有数据到达,操作系统将数据复制到应用程序指定的接收缓冲区。处理数据:应用程序可以对接收到的数据进行处理,如解析数据内容、进行数据处理等。
发送响应(可选):根据业务逻辑,应用程序可以选择发送响应数据给对方。
关闭连接:使用
close()
函数关闭TCP连接。可以选择在双方都完成数据传输后关闭连接,或根据应用程序的需求决定何时关闭连接。
ok,准备工作差不多了,来快乐地写代码吧🥰🥰🥰🥰
二、Windows下C语言 socket编程
2.1 <Winsock2.h>详解
2.11 库的引入和初始化
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
第二行的静态库加载是必要的,因为这不是C语言的标准库,编译的时候编译器不会主动帮你连接到库。
WSADATA
是 Windows Sockets Data 结构体的类型定义,它用于在使用 Winsock 库进行网络编程时存储相关的初始化和版本信息。
WSADATA
结构体包含了用于存储 Winsock 库初始化后的信息和状态的字段。在使用 Winsock 库之前,我们需要在应用程序中声明一个 WSADATA
类型的变量,并在初始化 Winsock 库时将其传递给相应的函数,以便获取初始化的状态和版本信息。
以下是 WSADATA
结构体的定义:
typedef struct _WSADATA {
WORD wVersion; // 请求的 Winsock 版本
WORD wHighVersion; // 支持的最高 Winsock 版本
char szDescription[WSADESCRIPTION_LEN+1]; // 描述信息
char szSystemStatus[WSASYS_STATUS_LEN+1]; // 系统状态
unsigned short iMaxSockets; // 支持的最大套接字数
unsigned short iMaxUdpDg; // 支持的最大 UDP 数据报大小
char* lpVendorInfo; // 供应商信息
} WSADATA;
当调用 WSAStartup
函数初始化 Winsock 库时,将会填充 WSADATA
结构体的相应字段,提供关于 Winsock 库的详细信息。我们可以通过检查 wVersion
字段来确保请求的 Winsock 版本被成功初始化,并且根据需要使用其他字段中的信息。
WSAStartup
函数是 Windows Sockets 启动函数,用于初始化 Winsock 库的使用。
函数原型如下:
int WSAStartup(
WORD wVersionRequested,
LPWSADATA lpWSAData
);
参数说明:
-
wVersionRequested
:请求的 Winsock 版本,以 WORD 类型表示。通常使用宏MAKEWORD
创建所需的版本号,例如MAKEWORD(2, 2)
表示请求使用版本 2.2。 -
lpWSAData
:指向WSADATA
结构体的指针,用于接收初始化后的 Winsock 信息和状态。
函数返回值:
- 如果调用成功,返回值为零(
0
)。 - 如果调用失败,返回值为非零,可以通过调用
WSAGetLastError
获取错误代码。
在使用 Winsock 相关函数之前,需要先调用 WSAStartup
函数初始化库,以确保库的正确运行和版本匹配。
在成功调用 WSAStartup
后,会填充 lpWSAData
参数指向的 WSADATA
结构体,其中包含了有关 Winsock 库的详细信息和状态。我们可以检查 WSADATA
结构体中的字段,如 wVersion
,以确保请求的 Winsock 版本已成功初始化。
在使用完 Winsock 库后,应调用 WSACleanup
函数来释放 Winsock 资源。 且分别在使用 Winsock 函数之前和之后分别调用。
示例:
#include <stdio.h>
#include <winsock2.h>
int main() {
WSADATA wsaData;
WORD wVersionRequested = MAKEWORD(2, 2);
// 初始化 Winsock 库
int result = WSAStartup(wVersionRequested, &wsaData);
if (result != 0) {
printf("WSAStartup failed: %d\n", result);
return 1;
}
// 使用 Winsock 库进行网络编程
// ...
// 释放 Winsock 资源
WSACleanup();
return 0;
}
2.12 常量和结构体
这些常量和结构体提供了表示网络地址、协议族和套接字类型的方式,并在网络编程中被广泛使用。通过使用这些常量和结构体,开发者可以方便地指定地址、端口和协议,并在套接字编程中进行地址转换、绑定、连接等操作。
常量:
-
AF_INET
:表示 IPv4 地址族。 - AF_INET6:表示 IPv6 地址族。
-
SOCK_STREAM
:表示流式套接字,用于 TCP 协议。 -
SOCK_DGRAM
:表示数据报套接字,用于 UDP 协议。 -
IPPROTO_TCP
:表示 TCP 协议。 -
IPPROTO_UDP
:表示 UDP 协议。 - INADDR_ANY:表示通配地址,用于绑定套接字时指定任意可用的本地地址。
- INADDR_LOOPBACK:表示回环地址,用于本地测试和通信。
结构体:
-
sockaddr
:- 描述:用于表示套接字的地址信息。
- 成员:
- sa_family:地址族,通常为 AF_INET 或 AF_INET6。
- sa_data:存储地址信息的字节流。
struct sockaddr{
sa_family_t sin_family; //地址族(Address Family),也就是地址类型
char sa_data[14]; //IP地址和端口号
};
-
sockaddr_in
:- 描述:用于表示 IPv4 套接字的地址信息。
- 成员:
- sin_family:地址族,固定为 AF_INET。
- sin_port:16 位端口号。
- sin_addr:32 位 IP 地址。(是个结构体)
- sin_zero:用于填充字节,使结构体大小与 sockaddr 保持一致。
struct sockaddr_in{
sa_family_t sin_family; //地址族(Address Family),也就是地址类型
uint16_t sin_port; //16位的端口号
struct in_addr sin_addr; //32位IP地址
char sin_zero[8]; //不使用,一般用0填充
};
其中,sin_addr为:
struct in_addr{
in_addr_t s_addr; //32位的IP地址(4字节的整数)
};
- sockaddr_in6:
- 描述:用于表示 IPv6 套接字的地址信息。
- 成员:
- sin6_family:地址族,固定为 AF_INET6。
- sin6_port:16 位端口号。
- sin6_flowinfo:流信息,用于区分数据流。
- sin6_addr:128 位 IPv6 地址。
- sin6_scope_id:用于区分接口的范围标识符。
重要说明:
套接字的地址信息是很重要的内容。
socket函数中的参数列表中,都是:sockaddr*
类型的参数,即指向sockaddr类型的指针,也就是上面的第一个结构体。
但是:sockaddr结构体中,将ip、地址和端口号合起来了,不方便我们操作;而第二个结构体sockaddr_in
,做的很好,很清晰。
很难给 sockaddr 类型的变量赋值,所以使用 sockaddr_in
来代替。这两个结构体的长度相同,强制转换类型时不会丢失字节,也没有多余的字节。
所以常见的操作是,使用sockaddr_in设置参数,使用 ,再强制类型转换为:sockaddr 类型。
如:
//创建套接字
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//创建sockaddr_in结构体变量
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr)); //每个字节都用0填充
serv_addr.sin_family = AF_INET; //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址,使用这个函数将字符串转为对应的类型
serv_addr.sin_port = htons(1234); //端口
//将套接字和IP、端口绑定
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
最后注意,结构体名称不是结构体地址。
2.13 函数原型
注意:UDP和TCP的接收、发送函数是不同的,因为他们一个面向连接、一个无连接嘛。
注意参数和返回值。
-
socket():
- 函数原型:
SOCKET socket(int af, int type, int protocol)
; - 描述:创建一个套接字。
- 参数:
- af:指定地址族,如 AF_INET(IPv4)或 AF_INET6(IPv6)。
- type:指定套接字类型,如 SOCK_STREAM(TCP)或 SOCK_DGRAM(UDP)。
- protocol:指定协议,通常为 0,根据套接字类型和地址族自动选择合适的协议。
- 返回值:成功:返回一个新创建的套接字描述符;失败:
INVALID_SOCKET
。
- 函数原型:
-
bind():
- 函数原型:
int bind(SOCKET s, const sockaddr* name, int namelen);
- 描述:将套接字绑定到指定的本地地址和端口。
- 参数:
- s:套接字描述符。
- name:指向 sockaddr 结构的指针,表示要绑定的地址(通常我们使用sockaddr_in结构体,然后类型转换为sockaddr )。
- namelen:sockaddr 结构的长度。
- 返回值:成功返回 0,失败返回
SOCKET_ERROR
。
- 函数原型:
-
listen():
- 函数原型:
int listen(SOCKET s, int backlog);
- 描述:开始监听套接字上的连接请求。
- 参数:
- s:服务端的套接字描述符。
- backlog:等待处理的连接请求的最大数量。
- 返回值:成功返回 0,失败返回 SOCKET_ERROR。
- 函数原型:
-
accept():
- 函数原型:
SOCKET accept(SOCKET s, sockaddr* addr, int* addrlen);
- 描述:接受客户端的连接请求,并创建一个新的套接字用于与客户端通信。
- 参数:
- s:服务端套接字描述符。
- addr:指向 sockaddr 结构的指针,用于存储客户端的地址信息。
- addrlen:addr 缓冲区的长度。
- 返回值:返回新创建的套接字描述符(后面通信用这个,而不是原来的客户端套接字),失败返回 INVALID_SOCKET。
- 函数原型:
-
connect():
- 函数原型:
int connect(SOCKET s, const sockaddr* name, int namelen);
- 描述:与指定的目标套接字建立连接。
- 参数:
- s:客户端套接字描述符。
- name:指向 sockaddr 结构的指针,表示目标套接字的地址。
- namelen:sockaddr 结构的长度。
- 返回值:成功返回 0,失败返回 SOCKET_ERROR。
- 函数原型:
-
send()
:用在TCP中- 函数原型:
int send(SOCKET s, const char* buf, int len, int flags);
- 描述:发送数据给连接的对方,用在TCP中。
- 参数:
- s:要发送数据的套接字描述符(connect函数返回的那个)。
- buf:指向要发送数据的缓冲区。
- len:要发送的数据长度。
- flags:可选的标志,如 MSG_DONTROUTE、MSG_OOB 等。
- 返回值:成功返回发送的字节数,失败返回 SOCKET_ERROR。
- 函数原型:
-
sendto()
: 用在UDP中- 函数原型:
int sendto(int socket, const void *buffer, int length, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
- 描述:发送数据给对方,用在UDP中。
- 参数:
- socket: 套接字描述符
- buffer:待发送数据的缓冲区指针
- ength:待发送数据的长度
- flags :可选的标志参数
- dest_addr:目标地址的结构体指针
- addrlen:目标地址结构体的长度
- 返回值:返回值表示成功发送的字节数,返回值为 -1 表示发送失败。
- 函数原型:
-
recv()
:用在TCP中- 函数原型:
int recv(SOCKET s, char* buf, int len, int flags);
- 描述:接收来自连接对方的数据。
- 参数:
- s:要接受数据的套接字描述符。
- buf:接收数据的缓冲区。
- len:buf 缓冲区的长度。
- flags:可选的标志,如 MSG_PEEK、MSG_WAITALL 等(设为0或NULL)。
- 返回值:成功返回接收到的字节数,失败返回 SOCKET_ERROR。
- 函数原型:
-
recvfrom()
:用在UDP中- 函数原型:
int recvfrom(int socket, void *buffer, int length, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
- 描述:用于从一个无连接套接字(如 UDP 套接字)接收数据
- 参数:
- socket:套接字描述符。
- buffer:接收数据的缓冲区指针。
- ength:缓冲区的长度,即可接收的最大数据量。
- flags:可选的标志参数,用于控制接收操作的行为。
- src_add:用于存储发送方地址信息的结构体指针。在调用
recvfrom
函数之后,该结构体将被填充为发送方的地址信息。 - addrlen:
src_addr
结构体的长度,需要传递一个指向socklen_t
类型的指针。
- 返回值:返回值表示实际接收到的数据字节数。如果返回值为 -1,则表示接收出错
- 函数原型:
-
closesocket():
- 函数原型:
int closesocket(SOCKET s);
- 描述:关闭套接字。
- 参数:s:要关闭连接的套接字描述符。
- 返回值:成功返回 0,失败返回 SOCKET_ERROR。
- 函数原型:
这些函数只是 winsock2.h 头文件中的一部分,用于创建、绑定、监听、接受、连接、发送和接收数据等常见的网络编程操作。通过这些函数,开发者可以构建各种网络应用程序,实现可靠的数据传输和网络通信。
补充1:
如果服务器和客户端的字节模式(一个大端模式、一个小端)不一样会怎么样,收到的数据顺序和发送的一致吗?
—— 一致。对于这种问题,数据在发送时统一采用网络字节序(即大端)。socket的发送、接收函数会自动完成发送和接收时的转换工作。
补充2:
有一些函数返回值或者参数类型是size_t
或者int
,或者ssize_t
。这里要稍微注意以下,虽然绝大数情况下不会出错,但可能有潜在风险。
-
int
:C语言的基本整数类型,为有符号整数,通常是32位; -
size_t
:无符号整数类型,表示对象大小、数组长度或内存块的字节数(比如sizeof返回值),它的值是非负的。很多编译器和平台上,他的定义可能是unsigned int
,但C语言标准并没有这样规定,有的平台也有可能被定义为unsigned long
。 -
ssize_t
:有符号整数类型,用来表示字节数或数据大小,通常用来表示读取和写入的结果。 - 但函数的参数类型和你传入的数据类型不完全相同时,应该注意类型转换。
2.2 UDP 套接字编程
函数,流程,前面都讲完了。这里直接上代码。
要说明的是:
- 同一电脑上运行他们,可以通信;
- 同一局域网运行,也可以连接(连接同一wifi时,你可以用手机软件做客户端或者服务端来测试);
- 服务端使用公网ip,也可以连接;
- 跨网络时,使用局域网地址是不能通信的。
2.21 服务端
sever的代码里面我写了详细注释,client就不写了哦。
#include <stdio.h>
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib") //加载 socket静态库
#define BUF_SIZE 1024 // 缓冲区大小
#define PORT 8888 // 端口号
int main() {
// 初始化 Winsock
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { //请求的winsock版本是winsock2
printf("WSAStartup failed.\n");
return 1;
}
SOCKET serverSocket; // 服务端、客户端套接字句柄
struct sockaddr_in serverAddr, clientAddr; // ipv4地址结构体
int clientAddrLen = sizeof(clientAddr); // 客户端地址结构体长度
char buffer[BUF_SIZE]; // 缓冲区
// 创建套接字
serverSocket = socket(AF_INET, SOCK_DGRAM, 0); // ipv4地址,数据报套接字,根据套接字自动选择协议类型(即UDP)
if (serverSocket == INVALID_SOCKET) {
printf("Failed to create socket.\n");
WSACleanup();
return 1;
}
// 设置服务器地址和端口
memset(&serverAddr, 0, sizeof(serverAddr)); //每个字节都用0填充
serverAddr.sin_family = AF_INET; // 协议
// serverAddr.sin_addr.s_addr = INADDR_ANY; // 地址(即0.0.0.0,监听本机所有网卡),写成:“127.0.0.1”也可以
serverAddr.sin_addr.s_addr = inet_addr("192.168.88.89"); //你自己改成你的哦
serverAddr.sin_port = htons(PORT); // 端口
// 绑定套接字
if (bind(serverSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
printf("Failed to bind socket.\n");
closesocket(serverSocket);
WSACleanup();
return 1;
}
printf("UDP server started. Waiting for data...\n");
// 接收和发送数据
while (1) {
int recvLen = recvfrom(serverSocket, buffer, BUF_SIZE, 0, (struct sockaddr*)&clientAddr, &clientAddrLen);
if (recvLen == SOCKET_ERROR) {
printf("Failed to receive data.\n");
break;
}
// 处理接收到的数据
buffer[recvLen] = '\0';
printf("Received data from client: %s\n", buffer);
if (strcmp(buffer, "exit") == 0) break; // 收到exit时退出
// 发送回应数据给客户端
const char* response = "I've got it.";
if (sendto(serverSocket, response, (int)strlen(response), 0, (struct sockaddr*)&clientAddr, clientAddrLen) == SOCKET_ERROR) {
printf("Failed to send response.\n");
break;
}
}
// 关闭套接字和清理 Winsock
closesocket(serverSocket);
WSACleanup();
return 0;
}
2.22 客户端
#include <stdio.h>
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib") //加载 socket静态库
#define BUF_SIZE 1024
#define SERVER_IP "192.168.88.89" // 这里是他要连接的服务端的ip地址和端口号
#define PORT 8888
int main() {
WSADATA wsaData;
// 初始化 Winsock
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
printf("WSAStartup failed.\n");
return 1;
}
SOCKET clientSocket;
struct sockaddr_in serverAddr; // 它连接的服务端的ip地址结构体
char buffer[BUF_SIZE];
// 创建套接字
clientSocket = socket(AF_INET, SOCK_DGRAM, 0);
if (clientSocket == INVALID_SOCKET) {
printf("Failed to create socket.\n");
WSACleanup();
return 1;
}
// 设置服务器地址和端口
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = inet_addr(SERVER_IP);
serverAddr.sin_port = htons(PORT);
while (1) {
printf("\nEnter message to send (max %d characters, exit to close):", BUF_SIZE);
fgets(buffer, BUF_SIZE, stdin);
buffer[strlen(buffer) - 1] = '\0';
// 发送数据到服务器
if (sendto(clientSocket, buffer, (int)strlen(buffer), 0, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
printf("Failed to send data.\n");
closesocket(clientSocket);
WSACleanup();
return 1;
}
if (strcmp(buffer, "exit") == 0)break;
// 接收服务器的回应数据
int serverAddrLen = sizeof(serverAddr);
int recvLen = recvfrom(clientSocket, buffer, BUF_SIZE, 0, (struct sockaddr*)&serverAddr, &serverAddrLen);
if (recvLen == SOCKET_ERROR) {
printf("Failed to receive response.\n");
closesocket(clientSocket);
WSACleanup();
return 1;
}
// 处理接收到的数据
buffer[recvLen] = '\0';
printf("Received response from server: %s\n", buffer);
}
// 关闭套接字和清理 Winsock
closesocket(clientSocket);
WSACleanup();
return 0;
}
功能测试:
2.3 TCP 套接字编程
2.31 服务端
#include <stdio.h>
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")
#define ip "192.168.81.89"
#define PORT 8080
#define BUF_SIZE 1024
int main() {
WSADATA wsaData;
// 初始化Winsock
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
printf("无法初始化Winsock\n");
return 1;
}
SOCKET serverSocket, clientSocket;
struct sockaddr_in serverAddr, clientAddress;
char buffer[BUF_SIZE];
// 创建服务器套接字
serverSocket = socket(AF_INET, SOCK_STREAM, 0);
if (serverSocket == INVALID_SOCKET) {
printf("无法创建套接字\n");
return 1;
}
// 设置服务器地址和端口
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
// serverAddress.sin_addr.s_addr = INADDR_ANY;
serverAddr.sin_addr.s_addr = inet_addr(ip);
serverAddr.sin_port = htons(PORT);
// 绑定套接字到指定的地址和端口
if (bind(serverSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
printf("绑定失败\n");
return 1;
}
// 监听传入的连接
if (listen(serverSocket, 1) == SOCKET_ERROR) {
printf("监听失败\n");
return 1;
}
printf("服务器正在监听端口 %d\n", PORT);
// 接受传入的连接
int clientAddressSize = sizeof(clientAddress);
clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddress, &clientAddressSize);
if (clientSocket == INVALID_SOCKET) {
printf("接受连接失败\n");
return 1;
}
printf("已经与客户端建立连接\n");
while (1) {
// 接收来自客户端的数据
memset(buffer, 0, sizeof(buffer));
int recvLen = recv(clientSocket, buffer, BUF_SIZE, 0);
if (recvLen == SOCKET_ERROR) {
printf("接收数据失败\n");
return 1;
}
buffer[recvLen] = '\0';
printf("从客户端接收到的数据:%s\n", buffer);
if (strcmp(buffer, "exit") == 0) break;
if (send(clientSocket, buffer, (int)strlen(buffer), 0) == SOCKET_ERROR) {
printf("发送响应失败\n");
return 1;
}
}
// 关闭连接
closesocket(clientSocket);
closesocket(serverSocket);
WSACleanup();
return 0;
}
2.32 客户端
#include <stdio.h>
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")
#define server_ip "192.168.81.89"
#define PORT 8080
#define BUF_SIZE 1024
int main() {
// 初始化Winsock
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
printf("无法初始化Winsock\n");
return 1;
}
SOCKET clientSocket;
struct sockaddr_in serverAddr;
char buffer[BUF_SIZE];
// 创建客户端套接字
clientSocket = socket(AF_INET, SOCK_STREAM, 0);
if (clientSocket == INVALID_SOCKET) {
printf("无法创建套接字\n");
return 1;
}
// 设置服务器地址和端口
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(clientSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
printf("连接服务器失败\n");
return 1;
}
printf("与服务器建立连接成功\n");
while (1) {
// 发送数据给服务器
memset(buffer, 0, sizeof(buffer));
printf("\n输入要发送的数据,exit退出:");
fgets(buffer,BUF_SIZE,stdin);
buffer[strlen(buffer) - 1] = '\0';
if (send(clientSocket, buffer, (int)strlen(buffer), 0) == SOCKET_ERROR) {
printf("发送数据失败\n");
return 1;
}
if (strcmp(buffer, "exit") == 0)break;
// 接收服务器的响应
memset(buffer, 0, BUF_SIZE);
if (recv(clientSocket, buffer, BUF_SIZE, 0) == SOCKET_ERROR) {
printf("接收响应失败\n");
return 1;
}
printf("从服务器接收到的响应:%s\n", buffer);
}
// 关闭连接
closesocket(clientSocket);
WSACleanup();
return 0;
}
功能测试:
三、Linux下C语言 socket编程(云服务器)
3.1 <sys/socket.h>详解
简单写个tcp的吧,到这里,大家应该都会了。
常量、结构、函数名和windows下几乎一样的,
它与window的Winsock2.h不同的地方有:
- 函数命名空间,Linux下是POSIX的命名空间,windows下是windows api的命名空间;
- 参数类型和返回类型;
- 头文件名称不同,windows上除了头文件还需要链接静态库;而Linux下不用,它是类unix系统的标准头文件;
- windows下需要使用WSAStartup函数来初始化,以及清理;Linux下不需要;
- 某些数据类型可能不同,有的是unsigned int,有的是int;
- 错误处理不同,sys/socket.h使用的是errno全局变量来表示错误代码;Winsock2.h使用WSAGetLastError函数来获取最后发生的错误代码。
- Linux下套接字也是“文件”。
3.11 常量和结构体
sys/socket.h
定义了许多常量和数据结构,用于在套接字编程中表示和处理网络地址、套接字选项和协议等。
绝大多部分和Winsock2.h
里面的差不多。
以下是其中一些常见的常量和数据结构的详细介绍:
1. 常量:
-
套接字域(domain)常量:
-
AF_UNIX
:本地域套接字(Unix域套接字)。 -
AF_INET
:IPv4套接字。 -
AF_INET6
:IPv6套接字。 -
AF_NETLINK
:Linux内核通信套接字。
-
-
套接字类型(type)常量:
-
SOCK_STREAM
:面向连接的流套接字,提供可靠的、基于字节流的通信(如TCP)。 -
SOCK_DGRAM
:无连接的数据报套接字,提供不可靠的、基于数据报的通信(如UDP)。 -
SOCK_RAW
:原始套接字,允许直接访问底层网络协议。
-
-
套接字选项常量:
-
SO_REUSEADDR
:允许地址重用,可以在套接字关闭后立即重用相同的本地地址。 -
SO_BROADCAST
:允许发送广播消息。 -
SO_KEEPALIVE
:启用套接字的保持活动功能,以检测连接是否断开。 -
SO_RCVBUF
:接收缓冲区大小的选项。 -
SO_SNDBUF
:发送缓冲区大小的选项。
-
-
协议常量:
-
IPPROTO_TCP
:TCP传输协议。 -
IPPROTO_UDP
:UDP传输协议。 -
IPPROTO_ICMP
:ICMP协议。
-
2. 数据结构:
-
struct sockaddr
:
通用的套接字地址结构体,用于表示各种套接字域的地址信息。它的成员包括:-
sa_family
:地址族,表示套接字的域。 -
sa_data
:地址数据。
-
-
struct sockaddr_in
:
IPv4的套接字地址结构体,用于表示IPv4地址信息。它的成员包括:-
sin_family
:地址族,通常为AF_INET
。 -
sin_port
:16位的端口号。 -
sin_addr
:IPv4地址。
-
-
struct sockaddr_in6
:
IPv6的套接字地址结构体,用于表示IPv6地址信息。它的成员包括:-
sin6_family
:地址族,通常为AF_INET6
。 -
sin6_port
:16位的端口号。 -
sin6_addr
:IPv6地址。 -
sin6_flowinfo
:流标识符。 -
sin6_scope_id
:范围ID。
-
-
struct sockaddr_storage
:
通用的套接字地址存储结构体,用于存储任意套接字地址信息。它的大小足够容纳任何可能的套接字地址结构体。
常用的常量、结构和windows下是一样的,注意的点也是一样的,这里就不重复了。
3.12 函数原型
下面是sys/socket.h
中一些常用函数的详细介绍:
-
int socket(int domain, int type, int protocol)
:- 用途:创建一个套接字。
- 参数:
-
domain
:套接字的域,如AF_INET
(IPv4)或AF_INET6
(IPv6)。 -
type
:套接字的类型,如SOCK_STREAM
(面向连接的流套接字)或SOCK_DGRAM
(无连接的数据报套接字)。 -
protocol
:套接字使用的协议,通常为0,表示根据域和类型自动选择默认协议。
-
- 返回值:成功时返回套接字的文件描述符,失败时返回-1。
-
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
:- 用途:将一个套接字绑定到一个地址。
- 参数:
-
sockfd
:要绑定的套接字的文件描述符。 -
addr
:指向要绑定的地址结构体的指针,可以是struct sockaddr
、struct sockaddr_in
或struct sockaddr_in6
等。 -
addrlen
:地址结构体的长度。
-
- 返回值:成功时返回0,失败时返回-1。
-
int listen(int sockfd, int backlog)
:- 用途:开始监听指定套接字上的连接请求。
- 参数:
-
sockfd
:要监听的套接字的文件描述符。 -
backlog
:等待连接队列的最大长度。
-
- 返回值:成功时返回0,失败时返回-1。
-
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
:- 用途:接受一个连接请求,返回新的套接字文件描述符。
- 参数:
-
sockfd
:监听套接字的文件描述符。 -
addr
:(可选)指向用于存储客户端地址信息的结构体指针。 -
addrlen
:(可选)指向addr
结构体长度的指针。
-
- 返回值:成功时返回新的套接字文件描述符,失败时返回-1。
-
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
:- 用途:与远程套接字建立连接。
- 参数:
-
sockfd
:要连接的套接字的文件描述符。 -
addr
:指向远程地址结构体的指针,可以是struct sockaddr
、struct sockaddr_in
或struct sockaddr_in6
等。 -
addrlen
:地址结构体的长度。
-
- 返回值:成功时返回0,失败时返回-1。
-
ssize_t send(int sockfd, const void *buf, size_t len, int flags)
:- 用途:发送数据到套接字,TCP。
- 参数:
-
sockfd
:要发送数据的套接字的文件描述符。 -
buf
:指向要发送数据的缓冲区。 -
len
:要发送的数据长度。 -
flags
:附加标志,可以是0或包含MSG_DONTWAIT
等选项的标志。
-
- 返回值:成功时返回发送的字节数,失败时返回-1。
-
ssize_t recv(int sockfd, void *buf, size_t len, int flags)
:- 用途:从套接字接收数据,TCP。
- 参数:
-
sockfd
:要接收数据的套接字的文件描述符。 -
buf
:用于接收数据的缓冲区。 -
len
:接收数据的缓冲区长度。 -
flags
:附加标志,可以是0或包含MSG_DONTWAIT
等选项的标志。
-
- 返回值:成功时返回接收的字节数,失败时返回-1。
-
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen)
:- 用途:向指定的目标地址发送数据报,UDP。
- 参数:
-
sockfd
:要发送数据的套接字的文件描述符。 -
buf
:指向要发送的数据的缓冲区。 -
len
:要发送的数据的长度。 -
flags
:附加标志,可以是0或包含MSG_DONTWAIT
等选项的标志。 -
dest_addr
:指向目标地址的结构体指针,可以是struct sockaddr
、struct sockaddr_in
或struct sockaddr_in6
等。 -
addrlen
:目标地址结构体的长度。
-
- 返回值:成功时返回发送的字节数,失败时返回-1。
-
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen)
:- 用途:从套接字接收数据报,并获取发送方的地址,UDP。
- 参数:
-
sockfd
:要接收数据的套接字的文件描述符。 -
buf
:用于接收数据的缓冲区。 -
len
:接收数据的缓冲区长度。 -
flags
:附加标志,可以是0或包含MSG_DONTWAIT
等选项的标志。 -
src_addr
:(可选)指向用于存储发送方地址信息的结构体指针。 -
addrlen
:(可选)指向src_addr
结构体长度的指针。
-
- 返回值:成功时返回接收的字节数,失败时返回-1。
-
int close(int sockfd)
:- 用途:关闭套接字。
- 参数:要关闭的套接字的文件描述符。
- 返回值:成功时返回0,失败时返回-1。
3.2 <arpa/inet.h> 介绍
前面的结构体中,ip地址、端口号这些通常都是整数类型,但我们输入的ip地址一般是点分十进制的字符串,需要进行转换。这就用到了<arpa/inet.h> 中的一些函数;前面windows的忘了讲了,都差不多,它定义在Winsock.h中的。
arpa:最早的分组交换网络,arpa也是一个mac地址和ip地址转换的协议;
inet:Internet。
下面是arpa/inet.h
头文件中提供的函数的函数原型、参数和返回值的详细介绍:
-
inet_addr
:将点分十进制ip地址转换为网络字节序的32位整数类型(in_addr_t)- 函数原型:
in_addr_t inet_addr(const char *cp);
- 参数:
cp
是一个指向以空字符结尾的字符串,表示点分十进制的IP地址。 - 返回值:如果转换成功,返回网络字节序的32位整数形式的IP地址;如果转换失败,返回
INADDR_NONE
(通常是 -1)表示错误。
- 函数原型:
-
inet_ntoa
:功能和前面的那个相反- 函数原型:
char *inet_ntoa(struct in_addr in);
- 参数:
in
是一个struct in_addr
结构,表示网络字节序的32位整数形式的IP地址。 - 返回值:返回一个指向静态缓冲区的指针,其中包含转换后的点分十进制形式的IP地址。需要注意的是,后续的调用会覆盖该缓冲区,因此应尽快使用转换后的字符串。
- 函数原型:
-
inet_ntop
:将网络字节序的ipv4、6地址(整数)转换为点分十(十六)进制的字符串- 函数原型:
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
- 参数:
af
是地址族,可以是AF_INET
(IPv4)或AF_INET6
(IPv6);src
是一个指向源地址的指针,可以是struct in_addr
或struct in6_addr
;dst
是一个指向目标字符串缓冲区的指针;size
是目标缓冲区的大小。 - 返回值:如果转换成功,返回指向目标字符串的指针;如果发生错误,返回
NULL
,并设置errno
表示错误原因。
- 函数原型:
-
inet_pton
:转换成2进制- 函数原型:
int inet_pton(int af, const char *src, void *dst);
- 参数:
af
是地址族,可以是AF_INET
(IPv4)或AF_INET6
(IPv6);src
是一个指向表示IP地址的字符串的指针;dst
是一个指向目标地址的指针,可以是struct in_addr
或struct in6_addr
。 - 返回值:如果转换成功,返回 1;如果转换失败,返回 0,并且
errno
表示错误原因。
- 函数原型:
-
htonl
:主机字节序和网络字节序的转换- 函数原型:
uint32_t htonl(uint32_t hostlong);
- 参数:
hostlong
是主机字节序的32位整数值。 - 返回值:返回网络字节序的32位整数值。
- 函数原型:
-
htons
:主机字节序和网络字节序的转换- 函数原型:
uint16_t htons(uint16_t hostshort);
- 参数:
hostshort
是主机字节序的16位整数值。 - 返回值:返回网络字节序的16位整数值。
- 函数原型:
-
ntohl
:主机字节序和网络字节序的转换- 函数原型:
uint32_t ntohl (uint32_t netlong);
- 参数:
netlong
是网络字节序的32位整数值。 - 返回值:返回主机字节序的32位整数值。
- 函数原型:
-
ntohs
:主机字节序和网络字节序的转换- 函数原型:
uint16_t ntohs(uint16_t netshort);
- 参数:
netshort
是网络字节序的16位整数值。 - 返回值:返回主机字节序的16位整数值。
- 函数原型:
3.3 Linux TCP 套接字编程
3.31 IP地址问题
在windows那一节我已经说了,只有在局域网内能相互通信,或者拥有公网ip。
对于云服务器,服务器厂商会给你一个ip地址,你可以用它来ssh登录之类的。但是要注意,它给你的地址可能不是你私有的,而是通过内网ip地址映射到公网ip的。
你用ifconfig -a看一下ether0网卡的ip地址和你登录的ip地址是否一致就可以判断了。
我两个服务器,一个是地址映射的(腾讯云),另一个是我独占的公网ip。
第二种情况,你可以直接绑定ip地址给套接字;第一种情况就不能直接用那个公网ip了.
你可以使用 0.0.0.0
或者 INADDR_ANY
。
另外,记得把相应的端口放行。
3.32 服务端代码和测试
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h> // linux下socket头文件
#include <arpa/inet.h> // ip地址转换、字节序转换
#define ip INADDR_ANY // 主机ip地址,表示监听主机所有网卡
//#define ip "0.0.0.0"
#define port 8087 // 端口号
#define BUF_SIZE 1024 //缓冲区大小
int main() {
int server_socket, client_socket;
struct sockaddr_in server_address, client_address;
char buffer[BUF_SIZE]; //缓冲区
// 创建套接字
server_socket = socket(AF_INET, SOCK_STREAM, 0);
if (server_socket == -1) {
printf("无法创建套接字\n");
return -1;
}
// 设置服务器地址和端口
memset(&server_address,0,sizeof(server_address));
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = ip;
//server_address.sin_addr.s_addr=inet_addr(ip);
server_address.sin_port = htons(port);
// 绑定套接字到指定的地址和端口
if (bind(server_socket, (struct sockaddr *)&server_address, sizeof(server_address)) < 0) {
printf("绑定失败\n");
return -1;
}
// 监听传入的连接
if (listen(server_socket, 1) < 0) {
printf("监听失败\n");
return -1;
}
printf("服务器正在监听端口 %d\n", port);
// 接受传入的连接
socklen_t client_address_length = sizeof(client_address); //注意这里的长度的类型是:socklen_t
client_socket = accept(server_socket, (struct sockaddr *)&client_address, &client_address_length);
if (client_socket < 0) {
printf("接受连接失败\n");
return -1;
}
printf("与客户端建立连接成功\n");
// 循环接受客户端请求,收到exit时关闭套接字
while(1){
memset(buffer, 0, sizeof(buffer));
ssize_t recvLen=recv(client_socket,buffer ,sizeof(buffer), 0);
if(recvLen<0){
puts("接收数据失败");
return -1;
}
buffer[recvLen]='\0';
printf("从客户端接收到的数据:%s\n",buffer);
if(strcmp(buffer,"exit")==0) break;
// 将收到的数据回送给客户端
if(send(client_socket,buffer,sizeof(buffer),0)<0){
puts("发送响应失败");
return -1;
}
}
// 关闭连接
close(client_socket);
close(server_socket);
return 0;
}
直接用的手机app来连接测试吧,都一样的。
服务器:
手机 app(socketdebugtools):
3.33 客户端代码和测试
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define ip "198.52.xx.xxx"
#define port 8087
#define BUF_SIZE 1024
int main() {
int client_socket;
struct sockaddr_in server_address;
char buffer[BUF_SIZE];
// 创建套接字
client_socket = socket(AF_INET, SOCK_STREAM, 0);
if (client_socket == -1) {
printf("无法创建套接字\n");
return -1;
}
// 设置服务器地址和端口
memset(&server_address,0,sizeof(server_address));
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr=inet_addr(ip);
server_address.sin_port = htons(port);
// 连接服务器
if (connect(client_socket, (struct sockaddr *)&server_address, sizeof(server_address))<0){
puts("与服务器建立连接失败");
return -1;
}
puts("与服务器建立连接成功");
while(1){
memset(buffer, 0, BUF_SIZE);
printf("输入要发送的数据(exit关闭两侧链接):");
fgets(buffer,BUF_SIZE,stdin);
buffer[strlen(buffer)-1]='\0';
if(send(client_socket,buffer,strlen(buffer),0)<0){
puts("发生数据失败");
return -1;
}
if(strcmp(buffer, "exit")==0) break;
memset(buffer, 0, BUF_SIZE);
if(recv(client_socket,buffer,sizeof(buffer),0)<0){
puts("从服务器接收响应失败");
return -1;
}
printf("从服务器接收响应为:%s\n",buffer);
}
// 关闭连接
close((int)client_socket);
return 0;
}
不在一个局域网,手机不方便做服务端了(路由器端口转发也不行,路由器ip也不是公网的)。
我另一个服务器有公网ip,用它做服务端(当然都运行在一台服务器上也行的)。
服务端:
客户端:
四、Python socket
4.1 socket 库 详解
socket
库是Python标准库的一部分,它提供了创建、连接和通信套接字的功能,使得开发网络应用程序变得简单和方便。以下是socket
库中一些常用函数和类的详细介绍:
函数:
-
socket.socket(family, type, proto=0)
:创建一个新的套接字对象。参数family
指定地址族(如socket.AF_INET
表示IPv4),type
指定套接字类型(如socket.SOCK_STREAM
表示TCP套接字),proto
指定协议。返回套接字对象。 -
socket.gethostname()
:获取当前主机的主机名。 -
socket.gethostbyname(hostname)
:根据主机名获取主机的IP地址。
套接字方法和属性:
-
socket.bind(address)
:将套接字绑定到指定的地址和端口。参数address
是一个元组,包含IP地址和端口号。 -
socket.listen(backlog)
:开始监听传入的连接。参数backlog
指定挂起连接队列的最大长度。 -
socket.accept()
:接受传入的连接,并返回一个新的套接字对象和客户端地址。 -
socket.connect(address)
:连接到指定的服务器地址和端口。参数address
是一个元组,包含IP地址和端口号。 -
socket.send(data)
:将数据发送到已连接的套接字。参数data
是要发送的字节流数据。 -
socket.recv(bufsize)
:从套接字接收数据。参数bufsize
指定每次最多接收的字节数。 -
socket.close()
:关闭套接字连接。
除了上述方法和属性,socket
对象还具有其他一些方法和属性,用于设置套接字的选项、获取有关套接字的信息等。
这只是socket
库的一些基本功能。根据需要,你还可以使用socket
库提供的其他函数和类来实现更复杂的网络应用程序,如设置套接字选项、使用多线程或异步操作处理多个连接等。文章来源:https://www.toymoban.com/news/detail-480727.html
4.2 服务端
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# @Time : 2023-6-2 上午 1:23
# @Author : 666
# @FileName: server_tcp
# @Software: PyCharm
# @Abstract : tcp服务端
import socket
# 定义主机和端口号
host = '127.0.0.1'
port = 8080
# 创建套接字对象
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 将套接字绑定到指定的主机和端口
server_socket.bind((host, port))
# 开始监听传入的连接
server_socket.listen(1)
print("服务器正在监听端口 {}:{}".format(host, port))
# 接受传入的连接
client_socket, address = server_socket.accept()
print("与客户端建立连接:{}".format(address))
# 接收来自客户端的数据
data = client_socket.recv(1024).decode('utf-8')
print("从客户端接收到的数据:", data)
# 发送响应给客户端
response = "服务器已接收到数据:{}".format(data)
client_socket.sendall(response.encode('utf-8'))
# 关闭连接
client_socket.close()
server_socket.close()
4.3 客户端
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# @Time : 2023-6-2 上午 1:24
# @Author : 666
# @FileName: client_tcp
# @Software: PyCharm
# @Abstract : tcp客户端
import socket
# 定义主机和端口号
host = '127.0.0.1'
port = 8080
# 创建套接字对象
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 连接服务器
client_socket.connect((host, port))
print("与服务器建立连接:{}:{}".format(host, port))
# 发送数据给服务器
data = "Hello, Server!"
client_socket.sendall(data.encode('utf-8'))
# 接收服务器的响应
response = client_socket.recv(1024).decode('utf-8')
print("从服务器接收到的响应:", response)
# 关闭连接
client_socket.close()
把 永 远 爱 你 写 进 诗 的 结 尾 ~ 文章来源地址https://www.toymoban.com/news/detail-480727.html
到了这里,关于【socket】从计算机网络基础到socket编程——Windows && Linux C语言 + Python实现(TCP+UDP)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!