简单讲一下基础知识,便于后面代码的理解,建议大概浏览一下这一小节内容。这里讲的只是冰山一角,建议大家学习计算机网络相关知识,推荐几本书:
互联网是一个巨大的网络系统,从工作方式上看,它由边缘部分和核心部分组成。
边缘部分是指连接到互联网的终端设备,如手机、电脑、路由器等;而核心部分则是指连接这些设备的网络基础设施。
边缘部分
核心部分
如路由器、交换机、网关和服务器等
。这些设备通过全球互联网络将数据从一个地方传输到另一个地方。核心部分是互联网的支持结构,它确保在用户设备之间进行有效地网络交换。计算机网络有很多分类方式,至少需要了解一些名词得含义(都是耳熟能详的),比如局域网、专用网、无线网等等。
(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)按照使用者划分
(3)按照传输介质分
(4)按照交换技术分
共享计算机网络的资源,以及在网中交换信息,就需要实现不同系统中的实体的通信。实体包括用户应用程序、文件传输信息包、数据库管理系统、电子邮件设备以及终端等。两个实体要想成功地通信,它们必须具有同样的语言。交流什么,怎样交流以及何时交流,都必须遵从有关实体间某种相互都能接受的一些规则,这些规则的集合称为协议。协议的关键成分如下。
体系结构是研究系统各部分组成及相互关系的技术科学。
计算机网络体系结构采用分层配对结构,用于定义和描述一组用于计算机及其通信设施之间互联的标准和规范的集合。遵循这组规范可以很方便地实现计算机设备之间的通信。也就是说,为了完成计算机之间的通信合作,把每台计算机互联的功能划分成有明确定义的层次,并规定了同层次进程通信的协议及相邻层之间的接口及服务,这些同层进程通信的协议以及相邻层的接口统称为网络的体系结构。
为了减小计算机网络的复杂程度,按照结构化设计方法,计算机网络将其功能划分成若干个层次(Lyer),较高层次建立在较低层次的基础上,并为更高层次提供必要的服务功能。
(1)基本概念
(2)层次结构的特点
(3)层次结构的优点
20 世纪 80 年代末和 90年代初,网络的规模和数量得到了迅猛的扩大和增长。但是许多网络都是基于不同的硬件和软件而实现的,这使得它们之间互不兼容。显然,在使用不同标准的网络之间是很难实现其通信的。为解决这个问题,国际标准化组织 ISO 研究了许多网络方案,认识到需要建立一种有助于网络的建设者们实现网络、并用于通信和协同工作的网络模型,因此在 1984年公布了开放式系统互连参考模型,称为 OSI/RM 参考模型(
Open System Interconnect Reference Model/Reference Model
),简称为OSI
参考模型。
上图:
相关概念:
(1) 层:开放系统的逻辑划分,代表功能上相对独立的一个子系统。
(2) 对等层:指不同开放系统的相同层次。
(3) 层功能:本层具有的通信能力,它由标准来指定。
(4) 层服务:本层向相邻高层提供的通信能力。根据 OSI 增值服务的原则,本层服务应是其所有下层服务与本层功能之和。
不展开讲了,本文是介绍socket的
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 分层体系结构中,网络接口层又称主机-网络层,它是最低层,负责接收网际层的 IP 数据报以形成帧发送到传输介质上;或者从网络上接收帧,抽取数据报交给互连层。它包括了能使用 TCP/IP 与物理网络进行通信的所有协议。TCP/IP 体系结构并未定义具体的网络接口层协议,旨在提高灵活性,以适应各种网络类型,如 LAN、WAN。它允许主机连入网络时使用多种现成的和流行的协议,例如局域网协
议或其他一些协议。
网际层
网际层又称互连层,是 TCP/IP 体系结构的第二层,它实现的功能相当于 OSI 参考模型中网络层的功能。网际层的主要功能如下。
传输层
传输层位于网际层之上,它的主要功能是负责应用进程之间的端到端通信。在 TCP/IP体系结构中,设计传输层的主要目的是在互连层中的源主机与目的主机的对等实体之间建立用于会话的端到端连接。因此,它与 OSI 参考模型的传输层相似。
应用层
应用层是最高层。它与 OSI 模型中的高 3 层的任务相同,都是用于提供网络服务,比如文件传输、远程登录、域名服务和简单网络管理等。
TCP/IP中各层的协议如图 :
我们每天都在使用近乎所有的协议。
这些协议都很有用,不展开了,下面介绍一下TCP和UDP协议。
前面的知识点或许你浏览一下就可以了,在下面这些知识点是socket编程中必备知识点。
网络地址就是网络中唯一标识网络中每台网络设备的一个数字,若没有这种唯一的地址,网络中的计算机之间就不可能进行可靠的通信。实际上网络中每个节点都有两类地址标识:数据链路层地址和网络层地址。
网络上的每一个设备有一个唯一的物理地址(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)
网络地址是逻辑地址,该地址可以通过操作系统进行定义和更改。网络地址采用一种分层编址方案,如同个人通信地址包括国家、省、市、街道、住宅号及个人姓名一样,网络分类逻辑化,越容易管理和使用,因而更加有用。(你可能在一个地方住一段时间,这段时间内,你的通信地址不变;当你换个地方住,出差、旅游的时候,你的地址又会发生改变)
在 TCP/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地址的编址方式经历了3个阶段:
(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年互联网仍然面临三个必须尽早解决的问题,这就是:
早在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 位网络地址
(1) 网络地址
在互联网中,经常需要使用网络地址,那么,怎么来表示一个网络地址呢?IP 地址方案规定,一个网络地址包含了一个有效的网络号和一个全“0”的主机号。
例如,地址 113.0.0.0 就表示该网络是一个 A 类网络的网络地址。而一个 IP 地址为202.100.100.2的主机所处的网络地址为 202.100.100.0,它是一个 C 类网络,其主机号为 2。
(2) 广播地址
当一个设备向网络上所有的设备发送数据时,就产生了广播。为了使网络上所有设备能够注意到这样一个广播,必须使用一个可识别和侦听的 IP 地址。通常,一个广播的标志是,其目的 IP 地址的主机号是全“1”。IP 广播有两种形式,一种叫直接广播,另一种叫有限广播。
例如 C 类地址 202.100.100.255 就是一个直接广播地址。互联网上的一台主机如果使用该 IP 地址作为数据报的目的 IP地址,那么这个数据报将同时发送到 202.100.100.0 网络上的所有主机。
显然,直接广播的一个主要问题是在发送前必须知道目的网络的网络号。
我们的电脑、移动设备一般只有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等。
因此,端口号经常与特定的协议相关联,以便网络设备和应用程序可以识别并将数据正确地传送到相应的服务或应用程序。然而,并非所有的端口号都与特定协议相关,因为在一些情况下,用户可以自定义端口号来与其特定应用程序关联。
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 代码或处理其他交互。
又说多了,这篇文章写不完了。
TCP/IP 体系结构的传输层定义了传输控制协议(TCP
,Transport Control Protocol)和用户数据报协议(UDP
,User Datagram Protocol)两种协议。它们利用 IP 层提供的服务,分别提供端到端可靠的和不可靠的服务。应用层协议通常都是基于他们的。
TCP
:
UDP
:
关于他们的报文帧格式、连接建立过程、流量控制等,此处先不介绍。
哎,不行,报文格式还是必须说一下的。
应用层的进程发送数据,是将数据依次向下传递给运输层、网络层等等,最后通过物理传输到达目的地,没向下传输一层,都要在数据前面添加相应层的首部,首都通常用来指明数据部分的长度、使用的协议版本、校验和等等信息,使得数据可以在各层进行正确的交付。
此外,每一层的报文长度都会受到一些限制,有协议本身的、也有设备限制,因此,是将数据全部塞进一个报文传输,还是每次只传输特定长度的数据呢?每次传多长好呢?
报文=首部+数据
数据就不用说了奥,来看看首部。
这是UDP的首部格式(不看位首部,那个是计算校验和用的):
UDP报文的首部很短,只有8字节。注意长度这个字段,表示整个UDP数据报的长度(单位:字节),占2个字节理论来说最大长度可以是: 2 16 2^{16} 216 字节,但数据部分往往远远达不到 2 16 − 8 2^{16}-8 216−8个字节。
它还受:
的限制,通常,数据部分应该小于:548字节。
具体的计算可以参考这篇文章:UDP传输报文大小详解
、
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的首部。如果一方没有数据要发送,也灭幼使用任何数据的首部俩确认收到的数据,在处理超时的许多情况中,也会发送不带你任何数据的报文段。
套接字用于在2个进程之间建立连接,就像一个套接管一样。
套接管(灵魂画手):
虽然说这名字挺有有道理,但真不好理解,很多人连套接管都不知道。我更喜欢叫他原名socket
(它的翻译时插座、插口)。
socket用来唯一标识网络中的一个通信连接,套接字=ip地址:端口号,如10.0.0.1:443
但是下面这些,也可以称为socket:
socket API
,并简称为socket。3和4,也可以称为:句柄。
句柄:(比如文件句柄、窗口句柄、内存句柄、对象句柄等等)也叫做描述符,它通常是一个指针,指向一个描述某个对象的数据结构,这个数据结构可以使对象的属性、方法、状态或者数据等等。句柄是一种抽象和封装,隐藏了许多底层的细节。(通常Windows下叫句柄,Linux下叫描述符)
套接字分为:原始套接字、流式套接字、数据报套接字:
下面先从简单的UDP套接字开始介绍。
各种编程语言、操作系统,都有相应的socket编程API,他们的函数、数据结构可能会有一些差异,但功能都是类似的。
下面是socket编程中常用的内容:
函数:
socket()
: 创建一个套接字bind()
: 将套接字与特定的地址和端口绑定listen()
: 监听传入的连接请求accept()
: 接受连接请求,创建一个新的套接字用于通信connect()
: 建立与远程套接字的连接send()/sendto():
发送数据recv()/recvfrom()
: 接收数据close()
: 关闭套接字常量:
AF_INET
: IPv4地址族AF_INET6:
IPv6地址族SOCK_STREAM
: 流式套接字,通常用于TCPSOCK_DGRAM
: 数据报套接字,通常用于UDPIPPROTO_TCP
: TCP协议IPPROTO_UDP
: UDP协议结构体:
这只是一些常见的函数、常量和结构体示例,实际上可能还有其他函数和相关的数据结构和常量,具体取决于编程语言和操作系统的支持。
后面还会具体介绍。
UDP套接字概述
UDP是一种面向数据报的传输协议,而UDP套接字则提供了对UDP协议的抽象接口。UDP套接字通过数据报进行通信,每个数据报是一个独立的、不可拆分的消息单元。UDP套接字以无连接的方式进行通信,不需要在发送和接收数据之前建立连接。
特点与优势
应用场景
UDP套接字在以下场景中得到广泛应用:
与TCP套接字的区别
UDP套接字与TCP套接字在特性上存在明显的区别:
TCP、UDP可以在很多模式下工作(对等、多播、广播),本文主要介绍经典的服务器-客户端模式(C/S),这也是实际应用中最常见的工作模式,下面的通信过程、代码,默认试试C/S模式。
当使用UDP套接字进行通信时,过程比较简单,以下是UDP套接字的通信过程的详细说明:
创建套接字:使用
socket()
函数创建一个UDP套接字。指定协议簇(如AF_INET)和套接字类型(如SOCK_DGRAM)。绑定套接字:使用
bind()
函数将套接字绑定到本地地址和端口。绑定套接字可以让操作系统知道该套接字要使用的本地地址和端口号。接收数据:调用
recvfrom()
函数等待接收来自网络中其他主机的UDP数据报。该函数会阻塞当前进程,直到有数据到达套接字。当数据到达时,操作系统将数据复制到应用程序指定的接收缓冲区,并返回数据的发送者的地址和端口。处理数据:应用程序可以对接收到的UDP数据报进行处理。这可能包括解析数据报的内容、验证数据的完整性、进行数据处理等操作。
准备发送数据:准备要发送的UDP数据报,包括目标地址和端口号以及要发送的数据内容。
发送数据:使用
sendto()
函数将UDP数据报发送给特定的目标地址和端口。可以指定目标地址为其他主机的IP地址和端口号。操作系统将数据报发送到网络中,并不关心是否成功到达目标主机。等待响应(可选):应用程序可以选择等待接收来自目标主机的响应数据。这需要调用recvfrom()函数等待接收响应数据报。
关闭套接字:使用
close()
函数关闭UDP套接字,释放相关的资源。
就不继续啰嗦介绍特点什么的了,前面有对比。
创建套接字:使用
socket()
函数创建一个TCP套接字。指定协议簇(如AF_INET)和套接字类型(如SOCK_STREAM)。绑定套接字(可选):如果是服务器端,可以使用
bind()
函数将套接字绑定到指定的本地地址和端口。这样客户端就可以连接到该地址和端口进行通信。如果是客户端,可以省略此步骤。监听连接(仅服务器端):如果是服务器端,使用
listen()
函数开始监听来自客户端的连接请求。指定同时允许多少个连接请求进入待处理队列。建立连接(仅客户端):如果是客户端,使用
connect()
函数连接到服务器端的指定地址和端口。该函数会阻塞当前进程,直到连接成功建立或发生错误。接受连接请求(仅服务器端):使用
accept()
函数接受客户端的连接请求。该函数会阻塞当前进程,直到有客户端连接进入。一旦连接被接受,将创建一个新的套接字来处理与该客户端的通信。发送数据:使用
send()
函数发送数据给连接的对方。可以将要发送的数据放入发送缓冲区。接收数据:使用
recv()
函数等待接收来自对方的数据。该函数会阻塞当前进程,直到有数据到达。一旦有数据到达,操作系统将数据复制到应用程序指定的接收缓冲区。处理数据:应用程序可以对接收到的数据进行处理,如解析数据内容、进行数据处理等。
发送响应(可选):根据业务逻辑,应用程序可以选择发送响应数据给对方。
关闭连接:使用
close()
函数关闭TCP连接。可以选择在双方都完成数据传输后关闭连接,或根据应用程序的需求决定何时关闭连接。
ok,准备工作差不多了,来快乐地写代码吧
#include
#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
#include
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;
}
这些常量和结构体提供了表示网络地址、协议族和套接字类型的方式,并在网络编程中被广泛使用。通过使用这些常量和结构体,开发者可以方便地指定地址、端口和协议,并在套接字编程中进行地址转换、绑定、连接等操作。
常量:
AF_INET
:表示 IPv4 地址族。SOCK_STREAM
:表示流式套接字,用于 TCP 协议。SOCK_DGRAM
:表示数据报套接字,用于 UDP 协议。IPPROTO_TCP
:表示 TCP 协议。IPPROTO_UDP
:表示 UDP 协议。结构体:
sockaddr
:
struct sockaddr{
sa_family_t sin_family; //地址族(Address Family),也就是地址类型
char sa_data[14]; //IP地址和端口号
};
sockaddr_in
:
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字节的整数)
};
重要说明:
套接字的地址信息是很重要的内容。
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));
最后注意,结构体名称不是结构体地址。
注意:UDP和TCP的接收、发送函数是不同的,因为他们一个面向连接、一个无连接嘛。
注意参数和返回值。
socket():
SOCKET socket(int af, int type, int protocol)
;INVALID_SOCKET
。bind():
int bind(SOCKET s, const sockaddr* name, int namelen);
SOCKET_ERROR
。listen():
int listen(SOCKET s, int backlog);
accept():
SOCKET accept(SOCKET s, sockaddr* addr, int* addrlen);
connect():
int connect(SOCKET s, const sockaddr* name, int namelen);
send()
:用在TCP中
int send(SOCKET s, const char* buf, int len, int flags);
sendto()
: 用在UDP中
int sendto(int socket, const void *buffer, int length, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
recv()
:用在TCP中
int recv(SOCKET s, char* buf, int len, int flags);
recvfrom()
:用在UDP中
int recvfrom(int socket, void *buffer, int length, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
recvfrom
函数之后,该结构体将被填充为发送方的地址信息。src_addr
结构体的长度,需要传递一个指向 socklen_t
类型的指针。closesocket():
int closesocket(SOCKET s);
这些函数只是 winsock2.h 头文件中的一部分,用于创建、绑定、监听、接受、连接、发送和接收数据等常见的网络编程操作。通过这些函数,开发者可以构建各种网络应用程序,实现可靠的数据传输和网络通信。
补充1:
如果服务器和客户端的字节模式(一个大端模式、一个小端)不一样会怎么样,收到的数据顺序和发送的一致吗?
—— 一致。对于这种问题,数据在发送时统一采用网络字节序(即大端)。socket的发送、接收函数会自动完成发送和接收时的转换工作。
补充2:
有一些函数返回值或者参数类型是size_t
或者int
,或者ssize_t
。这里要稍微注意以下,虽然绝大数情况下不会出错,但可能有潜在风险。
int
:C语言的基本整数类型,为有符号整数,通常是32位;size_t
:无符号整数类型,表示对象大小、数组长度或内存块的字节数(比如sizeof返回值),它的值是非负的。很多编译器和平台上,他的定义可能是unsigned int
,但C语言标准并没有这样规定,有的平台也有可能被定义为unsigned long
。ssize_t
:有符号整数类型,用来表示字节数或数据大小,通常用来表示读取和写入的结果。函数,流程,前面都讲完了。这里直接上代码。
要说明的是:
sever的代码里面我写了详细注释,client就不写了哦。
#include
#include
#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;
}
#include
#include
#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;
}
#include
#include
#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;
}
#include
#include
#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;
}
功能测试:
简单写个tcp的吧,到这里,大家应该都会了。
常量、结构、函数名和windows下几乎一样的,
它与window的Winsock2.h不同的地方有:
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下是一样的,注意的点也是一样的,这里就不重复了。
下面是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,表示根据域和类型自动选择默认协议。int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
:
sockfd
:要绑定的套接字的文件描述符。addr
:指向要绑定的地址结构体的指针,可以是struct sockaddr
、struct sockaddr_in
或struct sockaddr_in6
等。addrlen
:地址结构体的长度。int listen(int sockfd, int backlog)
:
sockfd
:要监听的套接字的文件描述符。backlog
:等待连接队列的最大长度。int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
:
sockfd
:监听套接字的文件描述符。addr
:(可选)指向用于存储客户端地址信息的结构体指针。addrlen
:(可选)指向addr
结构体长度的指针。int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
:
sockfd
:要连接的套接字的文件描述符。addr
:指向远程地址结构体的指针,可以是struct sockaddr
、struct sockaddr_in
或struct sockaddr_in6
等。addrlen
:地址结构体的长度。ssize_t send(int sockfd, const void *buf, size_t len, int flags)
:
sockfd
:要发送数据的套接字的文件描述符。buf
:指向要发送数据的缓冲区。len
:要发送的数据长度。flags
:附加标志,可以是0或包含MSG_DONTWAIT
等选项的标志。ssize_t recv(int sockfd, void *buf, size_t len, int flags)
:
sockfd
:要接收数据的套接字的文件描述符。buf
:用于接收数据的缓冲区。len
:接收数据的缓冲区长度。flags
:附加标志,可以是0或包含MSG_DONTWAIT
等选项的标志。ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen)
:
sockfd
:要发送数据的套接字的文件描述符。buf
:指向要发送的数据的缓冲区。len
:要发送的数据的长度。flags
:附加标志,可以是0或包含MSG_DONTWAIT
等选项的标志。dest_addr
:指向目标地址的结构体指针,可以是struct sockaddr
、struct sockaddr_in
或struct sockaddr_in6
等。addrlen
:目标地址结构体的长度。ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen)
:
sockfd
:要接收数据的套接字的文件描述符。buf
:用于接收数据的缓冲区。len
:接收数据的缓冲区长度。flags
:附加标志,可以是0或包含MSG_DONTWAIT
等选项的标志。src_addr
:(可选)指向用于存储发送方地址信息的结构体指针。addrlen
:(可选)指向src_addr
结构体长度的指针。int close(int sockfd)
:
前面的结构体中,ip地址、端口号这些通常都是整数类型,但我们输入的ip地址一般是点分十进制的字符串,需要进行转换。这就用到了
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地址。INADDR_NONE
(通常是 -1)表示错误。inet_ntoa
:功能和前面的那个相反
char *inet_ntoa(struct in_addr in);
in
是一个 struct in_addr
结构,表示网络字节序的32位整数形式的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
。errno
表示错误原因。htonl
:主机字节序和网络字节序的转换
uint32_t htonl(uint32_t hostlong);
hostlong
是主机字节序的32位整数值。htons
:主机字节序和网络字节序的转换
uint16_t htons(uint16_t hostshort);
hostshort
是主机字节序的16位整数值。ntohl
:主机字节序和网络字节序的转换
uint32_t ntohl (uint32_t netlong);
netlong
是网络字节序的32位整数值。ntohs
:主机字节序和网络字节序的转换
uint16_t ntohs(uint16_t netshort);
netshort
是网络字节序的16位整数值。在windows那一节我已经说了,只有在局域网内能相互通信,或者拥有公网ip。
对于云服务器,服务器厂商会给你一个ip地址,你可以用它来ssh登录之类的。但是要注意,它给你的地址可能不是你私有的,而是通过内网ip地址映射到公网ip的。
你用ifconfig -a看一下ether0网卡的ip地址和你登录的ip地址是否一致就可以判断了。
我两个服务器,一个是地址映射的(腾讯云),另一个是我独占的公网ip。
第二种情况,你可以直接绑定ip地址给套接字;第一种情况就不能直接用那个公网ip了.
你可以使用 0.0.0.0
或者 INADDR_ANY
。
另外,记得把相应的端口放行。
#include
#include
#include
#include // linux下socket头文件
#include // 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):
#include
#include
#include
#include
#include
#include
#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,用它做服务端(当然都运行在一台服务器上也行的)。
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
库提供的其他函数和类来实现更复杂的网络应用程序,如设置套接字选项、使用多线程或异步操作处理多个连接等。
#!/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()
#!/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()
把 永 远 爱 你 写 进 诗 的 结 尾 ~