网络
我们早习惯了网络的存在,无论是手机聊天,在线冲浪,还是打游戏,网络无处不在。在嵌入式系统中网络也非常重要,比如物联网,我们需要手机能够操作智能设备来完成功能,比如控制空调、电视、汽车等。所以学习网络的形成以及设计网络程序是非常重要的板块。
一:网络相关概念
1、互联网的发展概括
(1)为了更好应对和苏联的军事竞赛,1958美国成立了ARPA部门,开发了内部联网技术- ARPANET(因特网的前身)方便不同部门之间可以快速沟通,开发ncp协议作为早期网络的通信协议。
(2)70年代末,随着链接ARPANET的电脑节点不断增加,美国开发了tcp/ip,并代替旧的网络通信协议(ncp)作为军事内部的标准通信协议
(3)1983,ARPANET分裂成军用和民用两部分,民用部分随着全社会的加入,因特网从而开始崭露头角,这个是离不开tcp\ip协议的推进。
(4)1984,国际标准化组织(iso)发布了OSI七层网络通信协议,成为更全面、标准化的理论框架,但此时tcpip协议已经成为了实际应用的网络通信协议。
小结:实际计算机之间的通信中,tcp-ip是完成通信的协议,而osi七层模型是一个理论通信协议框架,osi对于网络通信过程的理解有帮助。
2、tcpip四层模型
(1)概念
是指整个网络通信协议家族的整合,它构成了互联网的基础通讯架构,为当前互联网的实际网络通信标准协议,实现了不同的计算机能够进行互联(因特网-Internet)。
协议:双方为了某个目的进行协定共同遵守的规定,比如结婚协议,学员管理协议等。网络协议指的是为了完成不同计算机之间进行数据通信而制定的规则或标准。网络协议有很多,不同的协议在网络通信的整个过程中发挥着不同的作用。
tcp、ip四层模型:指完成计算机网络通信的以tcp、ip协议为核心的整个网络协议集合(家族),根据处理 流程大致将这些协议分为四层。每一层都有很多网络协议进行协同工作。所有的网络协议协同工作完成了不同计算机之间的数据交互(网络通信)
(2)特点
- tcpi-p简化了osi七层模型,为4层模型,每层负责不同的通信任务
(3)具体模型
(4)每层的作用
tcp/ip中的层 | 功能 | TCP/IP协议族 |
---|---|---|
应用层 | 文件传输,电子邮件,文件服务,虚拟终端 | FTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet |
传输层 | 提供端对端的接口 | TCP, UDP |
网络层 | 为数据包选择路由 | IP,ICMP,RIP,OSPF,BGP,IGMP |
网络接口层 | 以二进制数据形式在物理媒体上传输数据 | ISO2110,IEEE802.1,EEE802.2 |
应用层:最靠近用户的一层,它直接为应用程序提供服务。它定义了用于软件应用程序之间通信和数据传输的接口和协议。
- 主要协议:HTTP(超文本传输协议)、FTP(文件传输协议)、SMTP(简单邮件传输协议)、DNS(域名系统)等
- 功能:为计算机上的应用程序提供数据的通信支持
传输层:传输层负责在网络中的两台主机之间进行可靠或不可靠的数据传输。它为两台主机的应用层之间提供端到端的通信
- 主要协议:TCP(传输控制协议)、UDP(用户数据报协议)。
- 功能:确保数据正确、有效地从源端传输到目的端。TCP提供可靠的数据传输,而UDP提供快速但不可靠的数据传输
- 要点:要强调该层负责数据传输。无论是第三方程序还是开发者自己写的程序
网络层:负责数据包从源到目的地的传输。它定义了数据包的路由选择和转发。
- 主要协议:IP(互联网协议)、ICMP(互联网控制消息协议)、ARP(地址解析协议)、RARP(反向地址解析协议)等。
- 功能:实现跨网络设备的数据传输。它将数据封装成数据包(IP数据报),并使用IP地址进行寻址和路由
网络接口层:网络接口层是TCP/IP协议栈中最底层,处理与物理网络的接口细节。它包括操作系统中的网络驱动程序和网络接口卡,负责在电子信号和数据包之间进行转换。
- 功能:实现数据在物理媒介(如电缆、光纤、无线电波)上的传输。这一层关心的是如何在网络媒介上以帧(frames)的形式传输数据
小结:在整个通信过程中,数据会从应用层向下经过每一层,每一层都会对数据进行封装,添加上该层所需要的头信息,直到网络接口层,然后在另一端,数据会逐层向上被解封装,直到应用层。
理解例子:寄快递
- 第一步:要寄什么东西(小物件一个小口袋就可以,大物品需要一个纸盒)。就像应用层。不同应用程序的数据可能用不同方式(http、smtp、ftp等)来包装数据,然后放送给下一层
- 第二步:将物品放到寄快递店里,并填写寄件地址和收件地址,可以选择是否优享达服务之类的。这个跟传输层一样,负责端到端的通信,那我得知道源端是谁,目的端是谁。同时为了保证数据正确到达,还会有是否有安全传输可以选择(tcp或udp)
- 第三步:快递员拿到快递之后就会返回到中转站进行分发,确保快递的准确可行路径(成都<卡车>—沈阳–中转站–目的地)。网络层也是一样,数据包的寻址和路由,并利用IP协议为每个发送和接收的设备提供一个地址(即IP地址),并决定数据包如何从源头送达目的地(确定正确的分发地址)。
- 第四步:确定路径之后就会通过飞机、火车、卡车等途径进行实际的寄送快递。网络接口层也是一样,通过光纤或WiFi等途径传输数据,到达另一个设备。
小结:应用层负责要传递或接受什么样的数据,传输层负责确定两端的地址,网络层负责路由的分发和寻址,网络接口层负责实际传输。
(5)数据解析(参考七层osi模型)
3、osi七层模型
(1)概念
OSI模型,即开放式通信系统互联参考模型(Open System Interconnection Reference Model),是国际标准化组织(ISO)提出的一个试图使各种计算机在世界范围内互连为网络的标准框架,简称OSI
(2)特点
- OSI模型是一个理想化 的模型,尚未有完整的实现
(3)七层模型
(4)每层的作用
OSI中的层 | 功能 | TCP/IP协议族 |
---|---|---|
应用层 | 文件传输,电子邮件,文件服务,虚拟终端 | TFTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet |
表示层 | 数据格式化,代码转换,数据加密 | 没有协议 |
会话层 | 解除或建立与别的节点的联系 | 没有协议 |
传输层 | 提供端对端的接口 | TCP,UDP |
网络层 | 为数据包选择路由 | IP,ICMP,RIP,OSPF,BGP,IGMP |
数据链路层 | 传输有地址的帧以及错误检测功能 | SLIP,CSLIP,PPP,ARP,RARP,MTU |
物理层 | 以二进制数据形式在物理媒体上传输数据。物理层的重点是确保数据比特能够在网络媒介上准确无误地传输 | ISO2110,IEEE802.1,EEE802.2 |
(5)讲解例子
以传输网页数据为例,讲解数据使用osi七层模型协议进行传输的大概过程:
应用层 (Application Layer):
- 浏览器是应用层的软件,它使用HTTP(或HTTPS,如果是加密的)来发起网页请求。当你输入 www.baidu.com 时,浏览器将这个请求转换为一个 HTTP GET 请求。
- 作用:将网页地址包装成一个数据(数据按照http协议来包装),包装后交给表示层继续处理
表示层 (Presentation Layer):
- 该层确保发送的数据在网络上可以被接收端理解。例如,如果浏览器需要加密请求(假设使用 HTTPS),则加密发生在这一层。同时也处理数据压缩和格式转换。
- 作用:将应用层发来的数据进行加密/解密,以及压缩/解压缩处理,处理后交给会话层处理。实际上tcp/ip协议处理完数据后交给传输层处理。
会话层 (Session Layer):
- 这一层在你的浏览器和服务器之间建立、管理和终止通信会话。它处理会话的开始和结束,并确保长时间无响应的会话被关闭。
- 确保浏览器和远程要通信的另一台电脑(服务器)建议里通信通道。
传输层 (Transport Layer):
- 浏览器决定使用 TCP(传输控制协议)来确保数据的可靠传输。HTTP 请求被分割成 TCP 数据包,并对它们进行排序和校验以确保无误的到达。其他数据可能采用udp来进行数据传输(比如视频和音频),
- 作用:决定采用tcp还是udp方式传输数据,决定后包装数据,传递给网络层进行处理
网络层 (Network Layer):
- 此时,请求需要被发送到互联网上。但是首先,通过 DNS 解析服务会将 www.baidu.com 转换成一个 IP 地址。接着,路由器使用 IP 协议来确定最佳路径以传递数据包到目标 IP 地址。将这些信息整合到上一层发来的数据后继续传递给下一层处理
数据链路层 (Data Link Layer):
- 数据包在这一层被转换成帧,并准备发送到本地网络。在这个层次中添加了物理地址(MAC地址)。 如果数据需要通过 Wi-Fi 或以太网传输,这个层次负责定义数据如何在这些物理媒体上格式化和发送。
物理层 (Physical Layer):
- 最后,数据在物理层以电信号传输到你的本地路由器,然后通过你的 ISP 到达互联网,最终到达服务器的物理硬件。在服务器端,这个过程以相反的顺序发生,将电信号转换回数据包,然后通向服务器的操作系统和软件。
在服务器处理完请求后(如查找网页内容、可能涉及到后台数据库查询等),响应数据会通过服务器的应用层、表示层、会话层、传输层、网络层、数据链路层和物理层重新发送回你的电脑。完成这个过程后,你的浏览器将收到的数据组装成一个可以显示的网页,然后你就可以看到 www.baidu.com 的主页了。
4、网络协议大全
协议名称 | 作用 |
---|---|
HTTP(应用) | 用于对浏览器等web服务提供网络服务,数据为网页内容 |
FTP | 文件传输协议,比如老师电脑向学生电脑传文件 |
SMTP | 简单邮件传输协议 |
DNS | 域名解析系统,www.woniuxy.com ->101.37.65.91 |
TCP(传输层) | 传输控制协议,提供可靠的连接导向服务 |
UDP | 用户数据报协议,提供无连接的快速传输服务 |
IP(网络层) | 网际协议,IP协议定义了地址系统(如IPv4和IPv6)、数据包结构和路由功能 |
ICMP | ICMP用于传递控制消息,比如传达网络中存在的问题。它常被用来诊断网络通信问题,例如“ping”命令就使用了ICMP来检测另一台计算机是否可达 |
ARP | 地址解析协议,ARP用于把网络层的IP地址转换为数据链路层的物理地址(如以太网MAC地址) |
RARP | 逆地址解析协议,RARP的作用与ARP相反,它由网络设备使用,用于将物理地址(MAC地址)转换为IP地址 |
OSPF | 开放最短路径优先,OSPF是一种内部网关协议,用于在单个自治系统(AS)内部确定路由的最短路径,使用算法动态计算出网络中各路由器到达目的地的最佳路径,ospf用于某个as内部 |
BGP | 边界网关协议,是互联网上用于路由和达成自治系统(AS)之间最佳路径决策的协议,bgp用于不同as之间的互联 |
Ethernet(数据链路层) | Ethernet(以太网)是最为广泛使用的局域网技术,提供了包括物理地址维护、帧分界和控制、错误检测和更正等服务 |
PPP | PPP是一个用于直接连接两端节点的协议,比如在用户和互联网服务提供商之间的连接 |
HDLC | 高级数据链路控制协议用于同步数据链路层的错误检查和流量控制 |
VLAN | VLAN技术允许在同一物理网络上创建多个逻辑子网,有助于管理大型网络,提高了安全性和网络管理的灵活性 |
ATM | ATM是一种以固定长度的单元进行数据传输的技术,这些单元在ATM网络中被称为细胞或单元。ATM适合于语音、视频和数据等类型的传输 |
IEEE 802.3(物理层技术标准) | 与以太网相关的一系列标准,定义了包括物理层电气和物理数据的接口的细节 |
V.35 | 在串行通信中用于较高数据速率的物理连接标准 |
EIA/TIA-232 | 原称RS-232,标准化了在串行通信端口上使用的信号,包括连接器的类型和使用 |
G.703 | 由国际电信联盟(ITU)定义的用于传输数字信号的物理层规格 |
5、总结
(1)协议
协议是指一种规则和约定,也可以用于指定一个事物的标准。比如网络协议就是用于网络中的数据交换方式和通讯过程应该是怎样的,或者员工管理、学员管理等是一种约定。
网络协议分很多种,而不同的网络协议负责网络通信中不同的部分。而实际中规定网络通信的是以tcp、ip为核心的协议族。而与之对应的osi七层模式是一种理想上的网络通信协议,但没有普及,更多的是一个指导作用。
每一层都有很多小的协议在协同一起完成该层的网络通信工作。
(2)OSI七层模型
- 物理层:最底层,负责在物理媒介(如电缆、光纤或无线电波)上转发原始的比特流
- 数据链路层:此层负责在相邻节点间的可靠链接,确保数据准确无误地传输。简单来说,它确保比特组成的帧在网络设备间传输没有错误。
- 网络层:负责将数据包从源头传到目的地,涉及不同网络间的路由选择。即数据包的地址以及选择最佳路径到达目的地
- 传输层:这一层确保数据的顺利传输,提供可靠的数据传输服务(也可以选择不可靠的udp)
- 会话层:会话层管理和维护网络中两个节点之间的通讯连接会话。它负责建立、管理以及断开通信会话。但在tcpip中,是传输层提供了会话的连接
- 表示层:此层负责数据的转换和编码,确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取。这一层也是被tcpip中的应用层给代替了。
- 应用层:最靠近用户的层,它为应用软件提供网络服务。
(3)tcp ip四层模式
- 应用层:代替osi前三层。为应用程序提供网络服务,包括数据的表示、编码和解码等
- 传输层:为两台主机上的应用程序提供端到端的通信,包括数据分段和重组,流量控制,错误检测和纠正
- 网络层:负责数据包的路由和转发,提供逻辑地址(如 IP 地址),确保数据包能够跨多种物理网络从源头到达目的地
- 链路层:负责在物理媒介上发送和接收数据帧,包括处理 MAC 地址(物理地址)、帧的封装和解封装、错误检测和修正。代替osi最后两层。
二:网络应用程序设计模式
计算机大多数应用程序都需要网络支持,而我们也可以称这些需要网络才能使用的应用程序为网络应用。根据设计不同将所有的网络应用简单分为两种设计模式
- C/S(Client/Server)架构:
- B/S(Browser/Server)架构:
1、两种设计模式
(1)C/S模式
传统的网络应用设计模式,客户机(client)/服务器(server)模式。需要在通讯两端各自部署客户机和服务器来完成数据通信。比如桌面应用程序或大多数游戏客户端
(2)B/S模式
浏览器(browser)/服务器(server) 模式。只需在一端部署服务器,另外一端使用每台PC都默认配置的浏览器即可完成数据的传输。比如浏览器。
2、优缺点
(1)C/S(Client/Server)架构
- 优点
- 网络自主性:一些客户端可以在没有网络或网络不稳定的情况下工作,比如格式工厂。
- 性能较高:客户端可以利用本地资源计算和处理数据,速度较快。
- 安全性:相对于B/S架构,客户端与服务器的通信可以更加容易地保证安全。
- 缺点
- 维护复杂:每个客户端的应用程序都需要单独安装和更新。
- 不易跨平台:不同操作系统的客户端需要开发不同版本的软件。
(2)B/S(Browser/Server)架构
- 优点
- 简化客户端维护:用户通过浏览器访问应用,不需要单独安装客户端软件。
- 跨平台:Web应用可以在不同操作系统的浏览器上运行。
- 易于升级维护:服务器端更新后,所有用户都能访问到最新版本的应用。
- 缺点
- 网络依赖性强:需要持续的网络连接。
- 安全性:Web应用相对容易受到网络攻击。
- 性能:相比本地应用,Web应用可能响应速度较慢。
(3)应用场景区别
- C/S架构:专业软件、游戏
- B/S架构:网页应用、在线办公
三:TCP 网络编程
A:网络套接字编程
1:网络编程
网络套接字编程是一种方式,使得不同的程序之间能够进行网络通信,通常涉及数据在不同的计算机之间的传输。套接字(Socket)是计算机网络数据传输的一个抽象层,它为我们提供了发送和接收数据的方式(编程语法)。通过网络套接字编程,我们可以实现不同设备之间或同一设备上的不同进程(或线程)之间的数据交换。根据数据传输使用协议的不同分为两种套接字编程
- tcp套接字编程
- udp套接字编程
2:套接字概念
套接字是一种抽象概念,用于描述不同计算机上的程序(或同一计算机上的不同进程)如何通过网络进行数据交换。在程序中,套接字表现为一个包含IP地址、端口号以及指定的协议(例如TCP或UDP)的数据结构。程序双方都各有一个套接字来描述该程序本身,方便在数据传输中进行身份确认,即一个套接字指向客户端,另一个套接字指向服务器。例子:文件流指针区分要操作的文件。套接字区分要访问网络的程序
一个套接字由 “协议 + IP 地址 + 端口号” 组成(TCP/UDP 中称为 “五元组”:源 IP、源端口、目标 IP、目标端口、协议),就像物理插座的形状和接口标准决定了能插入的设备类型,套接字的参数唯一确定了一个网络通信的端点。
而c语言提供了一套以套接字为核心的内置函数,用于开发者进行网络数据通信。函数内部隐藏了发送数据的细节,开发者不需要关心底层网络协议的运作,简化了网络编程难度。
注意:因为套接字编程是需要开发者自己编写程序,无法使用浏览器,所以采用c/s架构来编写。也就是说会有一个客户端程序和服务器端程序。
客户端:负责链接服务器端以及接受服务器返回的数据,当然也可以发送数据给服务器去处理。
服务器:负责接受和处理客户端连接的请求以及发送数据给客户端,也可以接受客户端发送过来的数据。
客户端连接服务器端程序的前提是服务器端得处于运行中。程序一定是先确保服务器处于正常运行中,才会运行客户端程序用于连接服务器并进行数据交互。
ip地址:每个电脑上网的编号(身份证)。是一串数字。但是对于用户来说记不住要访问服务器的ip地址。那么就可以使用域名来代替ip地址。例子。比如浏览器地址栏输入www.woniuxy.com,电脑就会根据该域名查找到对应服务器电脑的ip地址并进行访问。
端口号:电脑里的每个软件如果要上网,必须要操作系统为其分配一个端口号用于进行网络通信。**端口号本质上是一个数字,范围是065535**(2字节).系统预定占用的一般为01023,1023到50000之间是各个应用程序可以分配的端口号,后续的处于备用状态。比如远程登录软件一般使用22或443端口,浏览器一般使用8080端口来进行网络数据交互。
3:套接字分类
流式套接字(SOCK_STREAM):stream
- 提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复的发送且按发送顺序接收。内设置流量控制,避免数据流淹没慢的接收方。数据被看作是字节流,无长度限制。tcp套接字推荐使用
数据报套接字(SOCK_DGRAM):dgram
- 提供无连接服务。数据包以独立数据包的形式被发送,不提供无差错保证,数据可能丢失或重复,顺序发送,可能乱序接收。推荐udp使用
原始套接字(SOCK_RAW):
- 可以对较低层次协议如IP、ICMP直接访问。使用的少
B:TCP套接字简介
1:概念
顾名思义,tcp套接字是指使用tcp协议的网络套接字编程,意味着发送的数据能准确无误地从一个端点传输到另一个端点。而tcp本身是一种面向连接的、可靠的、基于字节流的传输层通信协议。在应用层看来,TCP 套接字提供了一个端到端的通信通道,允许两台主机上的应用程序交换数据流。
2:特点
可靠性:TCP提供可靠的数据传输服务,它确保数据正确无误地从发送端传输到接收端
错误检测:TCP通过校验和来检测数据在传输过程中的任何错误。如果检测到错误,相关数据包可以被丢弃并要求重新发送
拥塞控制:TCP能够监测网络中的拥塞,自动调整数据发送的速率来减少网络的负担
基于tcp以上的特点,我们在发送数据时,基本只需要考虑业务和代码的逻辑性,其他套接字会帮我们解决。
C:一些基本概念:
1:sockaddr
它用于表示套接字地址。这个结构体是通用的,意味着它可以被用来表示多种类型的网络地址,包括但不限于 IPv4 和 IPv6 地址。然而,在实际编程中,
sockaddr
结构体本身并不直接被操作,而是作为其特定于协议的派生结构体的基类存在。 由于
sockaddr
是一个通用的结构体,它包含了一个地址族字段(sa_family
),用于指示地址的类型(如 IPv4 或 IPv6)。这个字段对于系统来说至关重要,因为它决定了如何解释sockaddr
结构体中的其余部分。例如,如果sa_family
字段被设置为AF_INET
,那么系统就知道这个sockaddr
实际上是一个sockaddr_in
结构体,它包含一个 IPv4 地址和端口号。 通常使用
sockaddr_in
(对于 IPv4 地址)或sockaddr_in6
(对于 IPv6 地址)等派生结构体,而不是直接使用sockaddr
。这些派生结构体提供了更具体的字段来存储地址信息,使得编程更加直观和方便。然而,在将地址信息传递给系统调用或库函数时,程序员通常需要将派生结构体的指针转换为sockaddr
类型的指针,因为函数原型要求的是sockaddr
类型的参数。
2:sockaddr_in
(sockaddr_in
是 C 语言在 Linux/Unix 环境下用于存储 IPv4 网络地址信息的结构体,是网络编程的核心数据结构之一)
概念:为
<netinet/in.h>中
,用于描述一个网络通信地址,包含了地址相关信息,一般会配合connect()或accept()
进行使用组成
sin_family
: 地址家族(Address Family),通常是AF_INET
,指的是IPv4网络协议。sin_port
: 16位的端口号,存储网络字节顺序(通常使用htons()
函数来设置此值,确保字节顺序正确)。sin_addr
: 32位IP地址,存储网络字节顺序。其下有一个成员s_addr
,表示一个无符号长整型的IP地址。例子
struct sockaddr_in server; server.sin_family = AF_INET;//ipv4 server.sin_port = htons(8080);//设置端口 server.sin_addr.s_addr = inet_addr("127.0.0.1");//设置ip地址
sockaddr_in 详解
在网络编程中,要建立连接(如客户端连服务器)或监听端口(如服务器等客户端),必须明确 IP 地址和端口号。但计算机需要一种标准化的方式来存储和传递这些信息,于是 sockaddr_in
结构体应运而生:
- 它是 IPv4 地址的 “数据容器”,把 IP、端口等信息打包成一个结构体,方便系统函数(如
connect
、bind
)读取。 - 与系统函数配合:在调用
connect()
(客户端连服务器)或accept()
(服务器接受连接)时,需要传入一个指向sockaddr_in
的指针,告诉系统 “和谁通信” 或 “监听谁的连接”。
二、结构体组成详解
struct sockaddr_in {
sa_family_t sin_family; // 地址族(Address Family),如 AF_INET(IPv4)
in_port_t sin_port; // 16位端口号(网络字节序)
struct in_addr sin_addr; // 32位IP地址(网络字节序)
char sin_zero[8]; // 填充字节,确保结构体大小与 sockaddr 相同
};
// 其中,sin_addr 的定义:
struct in_addr {
uint32_t s_addr; // 32位无符号整数,表示IP地址
};
1. sin_family
(地址族)
- 作用:指定使用的协议族类型,告诉系统 “这个地址用的是哪种协议格式”。
- 常见值:
AF_INET
:表示 IPv4 协议(最常用)。AF_INET6
:表示 IPv6 协议(用于新版本互联网协议)。
- 注意:必须显式设置,否则系统可能默认其他值,导致连接失败。
2. sin_port
(端口号)
作用:存储通信使用的端口号(如 80、443、8080 等)。
关键点:
网络字节序:计算机存储数据有大端(高位在前,如网络传输)和小端(低位在前,如 x86 架构)之分。网络通信要求统一用 大端字节序(即 “网络字节序”)。
htons () 函数:用于将主机字节序转换为网络字节序(Host to Network Short)。
server.sin_port = htons(8080); // 正确!将本地的 8080 转为网络字节序 // 不要直接赋值!可能导致端口号解析错误 server.sin_port = 8080; // 错误!未转换字节序
3. sin_addr
(IP 地址)
作用:存储目标设备的 IP 地址。
存储方式:
- 实际存储在
sin_addr.s_addr
中,类型是uint32_t
(32 位无符号整数)。 - 但我们日常用的是 点分十进制表示法(如
127.0.0.1
),需要转换。
- 实际存储在
转换函数:
inet_addr():将点分十进制字符串转为uint32_t类型的网络字节序。
server.sin_addr.s_addr = inet_addr("127.0.0.1"); // 正确!转为网络字节序
INADDR_ANY:特殊值,表示 “任意 IP”(仅用于服务器绑定)。
server.sin_addr.s_addr = INADDR_ANY; // 服务器监听所有可用IP
三、使用示例:初始化 sockaddr_in
1. 客户端连接服务器(指定目标 IP 和端口)
struct sockaddr_in server;
server.sin_family = AF_INET; // 使用 IPv4 协议
server.sin_port = htons(8080); // 端口号 8080(转为网络字节序)
server.sin_addr.s_addr = inet_addr("127.0.0.1"); // 目标服务器 IP
// 然后调用 connect() 连接服务器
connect(sockfd, (struct sockaddr*)&server, sizeof(server));
2. 服务器监听端口(绑定本地 IP 和端口)
struct sockaddr_in server;
server.sin_family = AF_INET; // 使用 IPv4 协议
server.sin_port = htons(8080); // 监听 8080 端口
server.sin_addr.s_addr = INADDR_ANY; // 监听所有可用 IP(如 127.0.0.1、192.168.1.100 等)
// 然后调用 bind() 绑定地址,再 listen() 监听
bind(sockfd, (struct sockaddr*)&server, sizeof(server));
listen(sockfd, 5);
四、为什么需要强制类型转换?
在调用 connect()
、bind()
等函数时,你会注意到需要把 struct sockaddr_in*
强制转换为 struct sockaddr*
:
connect(sockfd, (struct sockaddr*)&server, sizeof(server));
这是因为这些系统函数的原型定义为接收 struct sockaddr*
类型的参数:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 历史原因:
struct sockaddr
是一个通用的地址结构,用于兼容不同协议族(如 IPv4、IPv6、Unix 域套接字等)。 - 内存布局:
struct sockaddr_in
和struct sockaddr
的前几个字节布局相同(都包含sin_family
),因此可以安全转换。
五、总结:sockaddr_in
的核心作用
- 存储 IPv4 地址信息:将 IP 地址、端口号等打包成一个结构体。
- 标准化接口:让系统函数(如
connect
、bind
)能统一处理不同协议的地址。 - 字节序转换:通过
htons()
、inet_addr()
等函数确保网络通信的字节序正确。
3:区别:
定义与用途
- sockaddr
- 定义:sockaddr是一个在C语言网络编程中使用的数据结构,用于表示套接字地址。它是一个通用的结构体,可以用于表示不同类型的套接字地址,如IPv4、IPv6等。
- 用途:主要用于在网络编程中定义和操作不同类型的网络地址。它通常与bind()、connect()等函数一起使用,以指定套接字的地址信息。
- sockaddr_in
- 定义:sockaddr_in是一个专门用于表示Internet地址(IPv4)和端口号的结构体。它定义在头文件<netinet/in.h>中,通常与套接字(socket)API一起使用。
- 用途:在网络编程中,程序员使用sockaddr_in来表示IPv4地址和端口号,从而建立网络连接。
- sockaddr
数据结构
- sockaddr
- 包含一个16位的地址族(sa_family),用于指定地址类型(如IPv4或IPv6)。
- 包含一个14字节的数据字段(sa_data),用于存储IP地址和端口号等信息。这个字段的具体格式取决于地址族。
- sockaddr_in
- 包含一个16位的地址族(sin_family),通常设置为AF_INET表示IPv4协议。
- 包含一个16位的端口号(sin_port),以网络字节序表示。
- 包含一个结构体 in_addr,用于表示32位的IP地址。
- 包含一个填充字段(sin_zero),通常设置为0,用于对齐或填充。
- sockaddr
使用上的差异
- sockaddr
- sockaddr是一个通用的结构体,程序员通常不直接操作它,而是将其用作函数参数,由操作系统进行内部处理。
- 由于sockaddr结构体可以表示不同类型的地址(如IPv4、IPv6),因此在处理不同类型的网络地址时,需要将其转换为相应的派生结构体(如sockaddr_in或sockaddr_in6)。
- sockaddr_in
- sockaddr_in是专为IPv4设计的结构体,程序员可以直接操作它来表示和建立网络连接所需的地址和端口号信息。
- 在使用sockaddr_in时,程序员需要设置其地址族、端口号和IP地址等字段,然后将其类型转换为sockaddr类型,以便传递给套接字API中的函数。
- sockaddr
总结:
sockaddr和sockaddr_in在结构和使用上有差异,但它们的内存大小一致,因此可以相互转换。
sockaddr是一个通用的结构体,用于表示不同类型的套接字地址;
sockaddr_in则是专为IPv4设计的结构体,用于表示Internet地址和端口号。在使用时,程序员应根据具体需求选择合适的结构体,并进行正确的类型转换。
4:字节序:
字节序(Endianness)是指在计算机系统中,多字节数据在内存中的存储顺序。主要有两种字节序:
- 大端序(Big-endian):
高位字节存储在起始地址。
以大端序为例,假设有一个 16 位数
0x1234
,它会被存储到内存中:高位字节
12
存储在 低地址(比如0x1000
)。低位字节
34
存储在 高地址(比如0x1001
)。
- 小端序(Little-endian):
低位字节存储在起始地址。
以小端序为例,同样的 16 位数
0x1234
,它会被存储到内存中:- 低位字节
34
存储在 低地址(比如0x1000
)。 - 高位字节
12
存储在 高地址(比如0x1001
)。
- 低位字节
- 什么是高位字节和低位字节?
对于一个多字节的数据(比如 16 位、32 位或 64 位的整数)
它的字节可以分成“高位字节”和“低位字节”:
高位字节:表示数据的较高有效部分(比如一个 16 位数
0x1234
中,12
是高位字节)。低位字节:表示数据的较低有效部分(比如
0x1234
中,34
是低位字节)。
- 什么是低地址和高地址?
内存是由一系列连续的地址组成的,每个地址可以存储一个字节(8 位)的数据。地址从低到高排列:
低地址:内存中较小的地址(比如
0x1000
)。高地址:内存中较大的地址(比如
0x1001
)。图示:
大端序和小端序示例
假设有一个 32 位整数 0x12345678:
大端序:
12 34 56 78
(从低地址到高地址)小端序:
78 56 34 12
(从低地址到高地址)
影响
- 数据交换:不同字节序的系统间传输数据时,需进行转换。
- 文件格式:某些文件格式规定了字节序,读取时需注意。
注意:
- 网络字节序,是大端字节序
- x86系统的CPU都是little-endian(小端)字节序
D:涉及函数
1:引入头文件
#include <arpa/inet.h>
#include <sys/socket.h>
//用于互联网地址族,包括 sockaddr_in 结构和 IP 地址定义。
#include <netinet/in.h>
2:socket 定义套接字
-- 语法
int socket(int domain, int type, int protocol);
-- 示例:得到一个tcp通信的套接字:
// 创建套接字
int server_fd = socket(AF_INET, SOCK_STREAM, 0); // 第三个参数:0代表自动选择
- 参数:
- domain: 指定套接字使用的协议族。常见的协议族有
AF_INET
(IPv4 网络协议),AF_INET6
(IPv6 网络协议),AF_UNIX
(本地通信,使用UNIX文件系统的路径名)。 - type: 套接字数据传输的类型信息。
SOCK_STREAM
(提供顺序化的、可靠的、双向的、基于连接的字节流,使用TCP协议)SOCK_DGRAM
(支持无连接的数据包服务,使用UDP协议)
- protocol: 计算机间通信中使用的协议信息。通常是
IPPROTO_TCP
用于 TCP 套接字,IPPROTO_UDP
用于 UDP 套接字。如果type
或domain
能唯一确定协议,这个参数也可以设置为0,让系统自动选择合适的协议
- domain: 指定套接字使用的协议族。常见的协议族有
- 返回值:
- 成功: 返回一个非负整数(≥ 0),表示新创建的套接字文件描述符,套接字文件描述符(如
sockfd
)是程序通过socket()
创建的新 FD,编号通常从 3 开始(因为 0-2 已被标准流占用),同文件IO - 失败: 返回-1
- 成功: 返回一个非负整数(≥ 0),表示新创建的套接字文件描述符,套接字文件描述符(如
3:bind<绑定端口号>
概念:绑定端口号
语法
#include <sys/socket.h> int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);
参数
int sockfd
:套接字文件描述符,是通过调用socket()
函数创建的。const struct sockaddr *addr
:一个指向sockaddr
结构的指针,该结构保存了要关联到套接字的地址(包括IP地址和端口号)。对于IPv4,这个结构会被类型转换为struct sockaddr_in
。socklen_t addrlen
:该地址的长度,对于IPv4是sizeof(struct sockaddr_in)
,对于IPv6是sizeof(struct sockaddr_in6)
。
返回值
- 成功时返回0
- 失败时返回-1
示例
struct sockaddr_in server_addr; server_addr.sin_family = AF_INET; // IPv4 协议族 server_addr.sin_port = htons(8080); // 端口号(网络字节序) server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有可用 IP // 或指定特定 IP:server_addr.sin_addr.s_addr = inet_addr("192.168.1.100"); // 强制转换为通用类型 bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
4:listen<监听端口号>
概念
- 监听指定的端口号:执行这步就可以认为是服务器已启动
语法
int listen(int sockfd, int backlog);
参数
int sockfd
:套接字文件描述符,是已经绑定到一个本地地址的套接字。int backlog
:指定了等待连接队列的最大长度。当有多个客户端同时尝试连接服务器时,服务器可能无法立即处理所有连接请求。backlog
参数定义了在拒绝新连接之前,连接请求的最大数量说明:
- 当连接请求到达时,如果服务器正在处理其他连接,新的连接请求会被放入等待队列。
- 如果等待队列已满(即队列中的连接请求数达到了
backlog
的值),新的连接请求会被拒绝,客户端可能会收到一个ECONNREFUSED
错误。 backlog
的值通常由系统决定,但可以通过这个参数进行调整。常见的值范围是5到10,但具体的最佳值取决于应用程序的需求和系统的负载能力
返回值
- 成功时返回0
- 失败时返回-1
监听的作用
- 监听通过
listen(sockfd, backlog)
实现:
sockfd:已绑定地址(IP + 端口)的套接字文件描述符(需先通过
socket()
和bind()
初始化)。backlog:连接队列的最大长度,包含两类连接:
- 未完成连接(Incomplete Connection):已收到客户端 SYN 包,但服务器尚未完成 ACK 响应(三次握手中间状态)。
- 已完成连接(Complete Connection):三次握手完成,等待服务器调用
accept()
取出。 - 注意:不同系统对
backlog
的实际处理逻辑有差异(如 Linux 会将其设为队列上限的 1.5 倍),需根据并发量合理设置(通常建议 5~128)。
状态转换图示
套接字创建(socket()) → 绑定地址(bind()) → 开始监听(listen()) → 接收连接(accept()) ↑ ↓ 主动套接字(客户端用) 被动套接字(服务器专用,监听状态)
- 监听通过
总结:监听(listen) 是服务器将套接字从 “准备连接” 转为 “被动接收连接” 状态的关键操作,通过创建并管理连接队列,使服务器能够有序处理多个客户端的连接请求。
5:accept<等待客户端连接>
概念
会阻塞程序执行并等待客户端的连接,有客户端连接后才执行后续的代码。
语法
int accept(int sockfd, struct sockaddr *client, socklen_t *addrlen);
参数
int sockfd
:这个参数是由socket()
函数返回的监听socket的文件描述符。该监听socket应已经绑定到一个本地地址和端口上,并且已经在这个端口上监听连接,即在之前已经调用过bind()
和listen()
函数。struct sockaddr *addr
:这是一个指向struct sockaddr
结构体的指针,这个结构体在函数调用成功后将被填充为接受连接的远程主机的地址信息。实际上,当accept
函数被调用时,系统会从监听队列中取出排在最前面的连接请求,创建一个新的套接字,并将请求连接的客户端的地址信息填充到这个结构体中。socklen_t *addrlen
:这是一个指向socklen_t
类型变量的指针,该变量在调用前应该被初始化为addr所指向的地址结构体的大小。accept
函数成功返回后,这个变量将被设置为实际接收到的地址的长度。
返回值
- 成功时,返回一个新的socket文件描述符,用于与接受的连接进行通信。此描述符代表一个已经建立的连接。
- 错误时,返回
-1
,并且errno
被设置为相应的错误码。
例子
struct sockaddr_in client_addr; socklen_t addrlen = sizeof(client_addr); // 接受连接 if ((new_socket = accept(server_fd, (struct sockaddr *)&client_addr, (socklen_t*)&addrlen)) == -1) { perror("accept"); close(server_fd); exit(EXIT_FAILURE); }
服务器端示例代码:
#define PORT 8080
#define BACKLOG 5
int main() {
int server_fd, client_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_addr_len = sizeof(client_addr);
// 1. 创建套接字
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 2. 准备服务器地址结构
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有可用接口
server_addr.sin_port = htons(PORT); // 转换网络端口字节序
// 3. 绑定套接字到指定地址和端口
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("Socket bound to port %d\n", PORT);
// 4. 使用listen()将套接字设置为监听状态
if (listen(server_fd, BACKLOG) == -1) {
perror("listen failed");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("Server listening on port %d with backlog %d\n", PORT, BACKLOG);
printf("Waiting for incoming connections...\n");
// 5. 接受客户端连接(实际项目中通常使用循环)
client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len);
if (client_fd == -1) {
perror("accept failed");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("Client connected: %s:%d\n",
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
// 6. 处理客户端请求(这里简化为打印信息后关闭连接)
const char *message = "Hello from server!";
send(client_fd, message, strlen(message), 0);//send() 是网络编程中用于发送数据的核心函数,在 TCP 通信里尤为常用
printf("Message sent to client\n");
// 7. 关闭连接
close(client_fd);
close(server_fd);
return 0;
}
6:connect< Client到Server>
概念:用于链接某个服务器
语法
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数
int sockfd
:这是指向一个打开的socket的套接字描述符,该socket是通过调用socket()
函数创建的。const struct sockaddr *addr
:这是一个指向struct sockaddr
结构体的指针,该结构体包含了目标服务器的地址和端口信息。通常,你会使用特定于地址族的结构体(例如对于IPv4是struct sockaddr_in
),并将其强制转换为struct sockaddr
类型的指针传递给connect
。socklen_t addrlen
:这是上述addr指向的地址结构的长度。
返回值
- 如果连接建立成功,
connect
函数返回0
。 - 如果连接失败,返回
-1
,并且errno
被设置为具体的错误代码
- 如果连接建立成功,
例子
if (connect(sockfd, (struct sockaddr *)&server, sizeof(server)) == -1) { perror("connect"); exit(-1); }
客户端端示例代码:
#define PORT 8080
#define SERVER_IP "127.0.0.1"
int main() {
int client_fd;
struct sockaddr_in server_addr;
char buffer[1024] = {0};
// 1. 创建套接字
client_fd = socket(AF_INET, SOCK_STREAM, 0);
if (client_fd == -1) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 2. 准备服务器地址结构
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
// 将IPv4地址从点分十进制转换为二进制形式
if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
perror("invalid address/ address not supported");
exit(EXIT_FAILURE);
}
// 3. 连接到服务器
if (connect(client_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("connection failed");
close(client_fd);
exit(EXIT_FAILURE);
}
printf("Connected to server %s:%d\n", SERVER_IP, PORT);
// 4. 接收服务器消息
ssize_t bytes_received = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
if (bytes_received == -1) {
perror("recv failed");
close(client_fd);
exit(EXIT_FAILURE);
} else if (bytes_received == 0) {
printf("Server closed connection\n");
} else {
buffer[bytes_received] = '\0';
printf("Server message: %s\n", buffer);
}
// 5. 关闭连接
close(client_fd);
return 0;
}
7:read<读取数据>
概念:用于从套接字或任何文件描述符中读取数据
语法
ssize_t read(int fd, void *buf, size_t count);
参数
int fd
:文件描述符,用于标识一个打开的文件或套接字。void *buf
:指向一个缓冲区的指针,这个缓冲区用于存储从文件描述符读取的数据。size_t count
:指示要读取的最大字节数。
返回值
- 成功时,返回读取的字节数,如果客户端关闭了,返回0
- 失败时,返回-1,并设置
errno
以指示错误类型。
8:write<写出数据>
概念:用于向套接字或任何文件描述符写入数据。
语法
ssize_t write(int fd, const void *buf, size_t count);
参数
int fd
:文件描述符,用于标识一个打开的文件或套接字。const void *buf
:指向一个包含要写入数据的缓冲区的指针。size_t count
:指示要写入的字节数。
返回值
成功时,返回写入的字节数。在许多系统中,
ssize_t
是和size_t
一样的宽度,但是是有符号的。例如,在一个32位系统上,size_t
通常是一个32位无符号整数,而ssize_t
是一个32位有符号整数失败时,返回-1,并设置
errno
以指示错误类型。
注意:它们都定义在
<unistd.h>
头文件中,这点对于普通文件I/O和套接字I/O都是一样的。当在套接字上使用read
和write
时,实际上是在进行网络I/O操作,这些操作底层由操作系统的网络堆栈来处理。而在普通文件上使用这些函数时,操作的是文件系统I/O。
9:close<关闭资源>
概念:关闭一个套接字或任何文件描述符
语法
int close(int fd);
参数
int fd
:要关闭的文件描述符或套接字的标识符。
返回值
- 成功时,
close
函数返回0
。 - 失败时,返回
-1
并设置全局变量errno
以指示错误的原因。
- 成功时,
-
10:recv<效果类似于read>
recv( )用于套接字编程,特别是在已建立的连接上接收数据。它的接口定义通常如下:
- 在Windows操作系统上:
int recv(SOCKET s, char *buf, int len, int flags);
- 在Linux操作系统上:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数解释如下:
s
或sockfd
:套接字描述符,用于标识一个已连接的套接字。buf
:指向用户空间的缓冲区,用于存放接收到的数据。len
:指定buf
的大小,即可以接收的最大数据长度。flags
:用于改变recv函数行为的标志位,可以是0或一些特定的标志组合。- flags:一组标志位,用于修改行为。常用的标志位包括:
0
:默认行为,不使用任何特殊标志.MSG_OOB
:发送带外数据.MSG_PEEK
: 查看数据但不移除接收缓冲区的内容(仅recv()
有效)。MSG_DONTWAIT
:非阻塞发送,如果数据不能立即发送,则立即返回.MSG_WAITALL
: 等待所有请求的数据到达后再返回(仅recv()
有效)。MSG_NOSIGNAL
:禁止在连接断开时触发SIGPIPE
信号(Linux 特有)。
- flags:一组标志位,用于修改行为。常用的标志位包括:
11:send<发送数据>
在C语言的网络编程中,
send()
函数用于发送数据到已连接的套接字。它通常用于TCP连接中的客户端或服务器端,用于发送数据到对端。send()
函数的原型如下
语法:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数解释
- sockfd:套接字描述符,这个套接字必须已经通过
connect()
函数与目标地址建立了连接。 - buf:指向要发送的数据缓冲区的指针.
- len:要发送的数据的长度,以字节为单位.
- flags:一组标志位,用于修改行为。常用的标志位包括:
0
:默认行为,不使用任何特殊标志.MSG_OOB
:发送带外数据。MSG_DONTWAIT
:非阻塞发送,如果数据不能立即发送,则立即返回.MSG_NOSIGNAL
:禁止在连接断开时触发SIGPIPE
信号(Linux 特有)
返回值
- 成功:返回实际发送的字节数.
- 失败:返回-1,并设置
errno
以指示错误原因.
12: 使用细节:
兼容性问题
- 部分标志(如
MSG_DONTWAIT
)是平台相关的(例如在 Windows 中不可用)。 - 需包含头文件
<sys/socket.h>
以使用这些标志。
- 部分标志(如
与
read()
/write()
的关系
以下两种写法完全等价:// 使用 send/recv(flags=0) send(sockfd, data, len, 0); recv(sockfd, data, len, 0); // 使用 write/read write(sockfd, data, len); read(sockfd, data, len);
TCP 与 UDP 的差异
- 对 TCP(流式协议),
flags
的影响较小(如MSG_WAITALL
可能无法严格保证)。 - 对 UDP(数据报协议),
flags
的行为更明确(如MSG_PEEK
会查看整个数据报)。
- 对 TCP(流式协议),
- 总结
- 默认行为:
flags = 0
表示标准读写操作,无附加控制。 - 高级需求:通过设置
flags
参数(如MSG_PEEK
)可实现精细控制。 - 一致性:若不需要特殊标志,优先用
read()
/write()
代码更简洁。
- 默认行为:
E:大概流程
客户端:
- 调用socket()创建一个套接字,并指定ip地址和使用协议,得到一个套接字描述符
- 调用Connect()尝试连接一个服务器
- 链接成功之后使用read()或write()进行接受服务器返回的数据或发送数据给服务器
- 不需要链接之后调用close()关闭套接字,释放套接字所占用的资源。
服务器端:
- 调用socket()创建一个套接字,并指定ip地址和使用协议,得到一个套接字描述符
- 调用bind()绑定一个端口号
- 调用listen用于监听绑定的端口,可以处理任何发送给该端口的信息(比如客户端发来的,需要accept来处理)
- 需要手动调用accept来接受客户端的链接
- 第四步成功之后就可以使用read()或write()来完成数据的交互
- 不需要链接之后调用close()关闭套接字,释放套接字所占用的资源。
F:示例代码:
完成一对一服务端和客户端的对话
服务端:
#include <stdio.h> // 标准输入输出(printf、perror 等) #include <stdlib.h> // 标准库(exit 等进程退出函数) #include <string.h> // 字符串操作(strlen 等) #include <unistd.h> // 系统调用(read、write、close 等 I/O 操作) #include <sys/types.h> // 基础系统类型(如 pid_t、size_t 等,为兼容历史代码) #include <sys/socket.h> // socket 核心函数(socket、bind、listen、accept 等) #include <arpa/inet.h> // 字节序转换(htons、inet_addr 等地址处理函数) #include <netinet/in.h> // 互联网地址结构(sockaddr_in 结构体定义) #define PORT 8080 // 定义服务器监听端口(可自定义,需注意避开系统保留端口) int main(int argc, char *argv[]) { // 1. 创建 socket:IPv4 协议、TCP 类型(SOCK_STREAM) int server_fd = socket(AF_INET, SOCK_STREAM, 0); if (server_fd < 0) { // 创建失败返回 -1 perror("socket() error!"); // 打印系统错误信息 exit(EXIT_FAILURE); // 直接退出进程 } /* socket(AF_INET, SOCK_STREAM, 0): AF_INET:用 IPv4 协议族。 SOCK_STREAM:选择 TCP 传输类型(可靠、面向连接)。 第三个参数 0:让系统自动选协议(TCP 对应 IPPROTO_TCP ,填 0 更灵活 )。 失败处理:创建 socket 失败(如资源不足、权限不够)时,打印错误并退出。 */ // 2. 准备绑定的地址结构(IPv4 专用) struct sockaddr_in server_addr = {0}; server_addr.sin_family = AF_INET; // 协议族:IPv4 server_addr.sin_port = htons(PORT); // 端口转换为网络字节序(大端) // 绑定本地回环地址(仅允许本机访问,若要外网访问,填 INADDR_ANY) server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");//根据 IPv4 协议,“127.0.0.1” 属于回环地址段(127.0.0.0/8),该段地址被永久保留,不会分配给任何物理网络接口。 // 执行绑定:将 socket 与 IP+端口关联 if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) { perror("bind() error!"); // 绑定失败(如端口被占用) close(server_fd); // 关闭无效 socket exit(EXIT_FAILURE); // 退出进程 } /* sockaddr_in 结构体:专门存 IPv4 地址信息,sin_family 固定 AF_INET,sin_port 需用 htons 转网络字节序(不同系统主机字节序可能不同,网络要求统一大端 )。 inet_addr("127.0.0.1"):把点分十进制 IP 转成网络字节序的整数。若填 INADDR_ANY(值为 0 ),则绑定所有本机 IP(适合外网访问场景 )。 bind 作用:让操作系统 “记住” 这个 socket 要监听的 IP 和端口,后续客户端才能通过该地址连接。 */ // 3. 启动监听:让 socket 进入被动监听状态,等待客户端连接 if (listen(server_fd, 5) == -1) { perror("listen() error!"); // 监听失败(如 socket 状态不对) close(server_fd); // 关闭 socket exit(EXIT_FAILURE); // 退出进程 } printf("服务器:正常监听 %d 端口\n", PORT); // 提示监听成功 /* listen(server_fd, 5): ◦ 第一个参数:要监听的 socket。 ◦ 第二个参数 5:内核为该 socket 维护的半连接队列长度(客户端发起连接但未完成三次握手的队列 ),超出则新连接会被拒绝。 • 成功后,socket 进入 “监听态”,可接收客户端连接请求 */ // 4. 等待并接受客户端连接 struct sockaddr_in client_addr = {0}; // 存客户端地址信息 socklen_t client_addr_len = sizeof(client_addr); // 地址结构体长度 // 阻塞等待客户端连接,成功返回新的 socket(client_fd)用于通信 int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len); if (client_fd == -1) { // 接受失败(如被信号中断) perror("accept() error!"); // 打印错误 close(server_fd); // 关闭监听 socket exit(EXIT_FAILURE); // 退出进程 } puts("--- 有客户端连接上来了 ---"); // 提示连接成功 /* accept 是阻塞函数:没客户端连接时,程序会停在这等待。 成功返回后: server_fd 仍用于监听新连接,client_fd 是专门与该客户端通信的新 socket(后续读写用它 )。 client_addr 会填充客户端的 IP 和端口信息(可用于记录、验证客户端来源 )。 */ // 5. 读写数据(与客户端通信) // 5-1. 读取客户端发送的数据 char buf[128] = {0}; // 存接收的数据 ssize_t count = read(client_fd, buf, sizeof(buf) - 1); // 读数据(-1 留位置存 '\0') if (count > 0) { // 读取到数据(count 是实际读的字节数) buf[count] = '\0'; // 手动补字符串结束符 printf("接收到 %ld 个字节数据。\n客户端说:%s\n", count, buf); } // 5-2. 回复客户端数据 char str[128] = "你好!客户端!"; // 要发送的内容 count = write(client_fd, str, strlen(str)); // 写数据(strlen 算有效长度) if (count > 0) { // 发送成功 printf("成功回复客户端:%ld 个字节消息。\n", count); } else if (count < 0) { // 发送失败(如客户端断开) perror("服务器写出消息失败!"); } else { // count == 0,客户端主动关闭连接 puts("客户端已经关闭了!"); } /* read:从 client_fd 读数据到 buf,返回实际读的字节数。若客户端正常关闭,返回 0;失败返回 -1。 write:往 client_fd 写数据(str 内容 ),返回实际写的字节数。需注意 TCP 是流式协议,收发需约定边界(或用应用层协议规范 )。 */ // 6. 关闭 socket(释放资源) close(server_fd); // 关闭监听 socket close(client_fd); // 关闭与客户端通信的 socket return 0; // 进程正常退出 /* close:释放 socket 占用的文件描述符、内核资源,确保系统资源不泄漏。 */ } /* 创建 socket → 2. 绑定 IP + 端口 → 3. 监听端口 → 4. 接受客户端连接 → 5. 收发数据 → 6. 关闭连接 这是最基础的 TCP 服务端 “一次连接” 模型(只能处理一个客户端连接,处理完就退出 )。 若要支持多客户端 / 持续服务,需结合 多进程(fork)、多线程 或 IO 多路复用(select/epoll) 改造,让服务端能同时 / 循环处理多个连接。 */ ----------------------------------------------循环------------------------------------------------------- #include <unistd.h> // read(),write()和close()等api #include <arpa/inet.h> // 提供字节序转换(比如主机字节序和网络字节序的转换)和地址转换的函数 #include <sys/socket.h> // socket(),connect(),accept(),bind(),listen()和shutdown() #include <netinet/in.h> // 用于地址族,sockaddr, sockaddr_in 结构体和 IP 地址定义 #define PORT 8080 int main(int argc, char *argv[]) { // 1: 创建socket int server_fd = socket(AF_INET, SOCK_STREAM, 0); // 第三个参数:0代表自动选择 if(server_fd < 0){ perror("socket() error!"); exit(EXIT_FAILURE); } // 2: 绑定ip和端口号: struct sockaddr_in server_addr = {0}; server_addr.sin_family = AF_INET;//ipv4 server_addr.sin_port = htons(PORT);//设置端口 server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");//设置ip地址 if(bind(server_fd, (struct sockaddr *)&server_addr , sizeof(server_addr)) == -1){ perror("bind() error!"); close(server_fd); exit(EXIT_FAILURE); } // 3: 监听端口 if(listen( server_fd, 5 ) == -1){ perror("listen() error!"); close(server_fd); exit(EXIT_FAILURE); } printf("服务器:正常监听%d端口\n", PORT ); // 4: 等待客户端连接上来 struct sockaddr_in client_addr = {0}; int client_addr_len = sizeof(client_addr); int client_fd; // 使用goto解决客户端关闭,退出问题 front: if( (client_fd = accept( server_fd , (struct sockaddr *)&client_addr , &client_addr_len )) == -1){ perror("accpet() error!"); close(server_fd); exit(EXIT_FAILURE); } puts("--- 有客户端连接上来了 ---"); // 5: 读和写(数据交换) // 5-1:先接收客户端的消息: char buf[128] = {0}; char str[128] = {0}; ssize_t count; while(true){ if( (count = read( client_fd , buf, sizeof(buf) - 1)) > 0 ){ // 手动补结束符 buf[count] = '\0'; printf("接收到%ld个字节数据。\n客户端说:%s\n",count, buf); }else if(count == 0){ puts("客户端已经关闭了!"); //break; goto front; //跳到 54行,重新等待客户端连接上来。 }else { perror("发送信息错误!"); break; } // 5-2:回复给客户端消息: puts("服务器说:"); scanf("%s", str); if(strcmp(str, "exit") == 0){ // 输入exit结束服务端程序 break; } count = 0; if((count = write( client_fd , str , strlen(str) ) ) > 0 ){ printf("成功回复客户端:%ld个字节消息。\n" , count); }else if(count < 0){ perror( "服务端写出消息失败!" ); break; } } // 6:关闭 close(server_fd); close(client_fd); return 0; }
客户端:
/*************************************************** # File Name: tcp客户端.c # Author: Super ze # Mail: 2592972473@qq.com # Created Time: Mon 16 Jun 2025 04:55:51 PM CST ****************************************************/ #include <unistd.h> // 系统调用(read、write、close 等 I/O 操作) #include <arpa/inet.h> // 字节序转换(htons、inet_addr 等地址处理函数) #include <sys/socket.h> // socket 核心函数(socket、connect 等) #include <netinet/in.h> // 互联网地址结构(sockaddr_in 结构体定义) #include <stdio.h> // 标准输入输出(printf、perror 等) #include <stdlib.h> // 标准库(exit 等进程退出函数) #include <string.h> // 字符串操作(strlen 等) #define PORT 8080 // 定义要连接的服务器端口(需与服务端端口一致) int main(int argc, char *argv[]) { // 1. 创建 socket:IPv4 协议、TCP 类型(SOCK_STREAM) int client_fd = socket(AF_INET, SOCK_STREAM, 0); if (client_fd < 0) { // 创建失败返回 -1 perror("socket() error!"); // 打印系统错误信息 exit(EXIT_FAILURE); // 直接退出进程 } /* socket(AF_INET, SOCK_STREAM, 0): AF_INET:用 IPv4 协议族。 SOCK_STREAM:选择 TCP 传输类型(可靠、面向连接)。 第三个参数 0:让系统自动选协议(TCP 对应 IPPROTO_TCP ,填 0 更灵活 )。 失败处理:创建 socket 失败(如资源不足、权限不够)时,打印错误并退出。 */ // 2. 准备服务端地址结构(要连接的目标) struct sockaddr_in server_addr = {0}; server_addr.sin_family = AF_INET; // 协议族:IPv4 server_addr.sin_port = htons(PORT); // 端口转换为网络字节序(大端) // 设置服务端 IP(这里是本地回环地址,也可填外网 IP) server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 执行连接:尝试与服务端的 IP+端口建立 TCP 连接 if (connect(client_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) { perror("connect() error!"); // 连接失败(如服务端未启动、网络不通) close(client_fd); // 关闭无效 socket exit(EXIT_FAILURE); // 退出进程 } puts("连接到服务器成功!"); // 提示连接成功 /* sockaddr_in 结构体:存服务端的 IP 和端口信息,sin_family 固定 AF_INET,sin_port 需用 htons 转网络字节序(适配不同系统的主机字节序 )。 inet_addr("127.0.0.1"):把点分十进制 IP 转成网络字节序的整数。若服务端在外网,填对应的公网 IP 即可。 connect 作用:发起 TCP 三次握手,尝试与服务端建立连接。成功后,客户端和服务端的 socket 就可以收发数据了。 */ // 3. 读写数据(与服务端通信) // 3-1. 发送数据给服务端 char buf[128] = {0}; // 存接收的数据 char str[128] = "hello,服务器!"; // 要发送的内容 ssize_t count = 0; // 记录收发的字节数 // 发送数据:往服务端写内容 if ((count = write(client_fd, str, strlen(str))) > 0) { printf("成功发送:%ld 个字节消息。\n", count); } // 3-2. 接收服务端回复的数据 count = 0; // 重置计数 // 读取服务端返回的数据 if ((count = read(client_fd, buf, sizeof(buf) - 1)) > 0) { buf[count] = '\0'; // 手动补字符串结束符 printf("接收到 %ld 个字节数据。\n服务端说:%s\n", count, buf); } /* write:往 client_fd 写数据(str 内容 ),返回实际写的字节数。因为 TCP 是流式协议,需注意收发双方的 “数据边界”(比如服务端要能正确解析客户端发送的内容 )。 read:从 client_fd 读数据到 buf,返回实际读的字节数。若服务端正常关闭连接,返回 0;失败返回 -1。 */ // 4. 关闭 socket(释放资源) close(client_fd); return 0; // 进程正常退出 } /* 创建 socket → 2. 连接服务端 → 3. 发送数据 → 4. 接收回复 → 5. 关闭连接 这是最基础的 TCP 客户端模型,逻辑简单直接:创建 socket 后主动连服务端,发消息、收回复,最后关闭。 */ --------------------------------------------循环--------------------------------------------------------- #include <stdio.h> #include <stdbool.h> #include <string.h> #include <stdlib.h> #include <unistd.h> // read(),write()和close()等api #include <arpa/inet.h> // 提供字节序转换(比如主机字节序和网络字节序的转换)和地址转换的函数 #include <sys/socket.h> // socket(),connect(),accept(),bind(),listen()和shutdown() #include <netinet/in.h> // 用于地址族,sockaddr, sockaddr_in 结构体和 IP 地址定义 #define PORT 8080 int main(int argc, char *argv[]) { // 1: 创建socket int client_fd = socket(AF_INET, SOCK_STREAM, 0); // 第三个参数:0代表自动选择 if(client_fd < 0){ perror("socket() error!"); exit(EXIT_FAILURE); } // 2: 连接到服务端: struct sockaddr_in server_addr = {0}; server_addr.sin_family = AF_INET;//ipv4 server_addr.sin_port = htons(PORT);//设置端口 server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");//设置ip地址 if(connect(client_fd, (struct sockaddr *)&server_addr , sizeof(server_addr)) == -1){ perror("connect() error!"); close(client_fd); exit(EXIT_FAILURE); } puts("连接到服务器成功!"); // 3: 读和写(数据交换) // 3-1:发送消息给服务端: char buf[128] = {0}; char str[128] = {0}; while(true){ puts("客户端说:"); scanf("%s", str); if(strcmp(str, "exit") == 0){ // 输入exit结束服务端程序 break; } ssize_t count = 0; if((count = write( client_fd , str , strlen(str) ) )> 0){ printf("成功发送:%ld个字节消息。\n", count ); } // 3-2: 接收服务器回复的消息 count = 0; if( (count = read( client_fd , buf, sizeof(buf) - 1)) > 0 ){ printf("接收到%ld个字节数据。\n 服务端说:%s\n",count, buf); } } // 4:关闭 close(client_fd); return 0; }
G:TCP的连接过程
3次握手,4次挥手
- 1:概念
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。在TCP/IP模型中,TCP提供了稳定、有序且无差错的数据传输。为了保证数据传输的可靠性,TCP使用了三次握手(three-way handshake)机制来建立一个连接,以及四次挥手(four-way handshake)机制来终止一个连接。
- 2:流程说明:
三次握手
- 流程
- 第一次握手:客户端向服务器发送一个带有 SYN(同步序列号)标志的 TCP 报文段,该报文段中还包含客户端随机生成的初始序列号(Sequence Number),表示客户端请求建立连接。
- 第二次握手:服务器接收到客户端的 SYN 报文段后,会向客户端发送一个带有 SYN 和 ACK(确认应答)标志的 TCP 报文段。其中,SYN 标志用于同步服务器的序列号,ACK 标志用于确认客户端的序列号,服务器会将客户端的序列号加 1 作为确认号(Acknowledgment Number),同时服务器也会生成自己的初始序列号。
- 第三次握手:客户端接收到服务器的 SYN + ACK 报文段后,会向服务器发送一个带有 ACK 标志的 TCP 报文段,确认号为服务器的序列号加 1,序列号为客户端在第一次握手中发送的序列号加 1。服务器接收到这个 ACK 报文段后,连接建立成功。
- 必要性
- 防止已失效的连接请求报文段突然又传送到服务器,从而产生错误。如果没有三次握手,客户端发送的连接请求可能会因为网络延迟等原因在网络中滞留,当客户端再次发送连接请求并成功建立连接后,之前滞留的连接请求到达服务器,服务器会误以为是新的连接请求而建立连接,造成资源浪费。
- 让客户端和服务器能够同步双方的序列号,确保数据传输的准确性和可靠性。通过三次握手,双方可以互相确认对方的初始序列号,从而在后续的数据传输中能够正确地对数据进行编号和确认。
- 使双方能够确认对方的接收和发送能力。在三次握手过程中,双方都能向对方发送数据并接收对方的确认,从而确保双方的发送和接收功能都正常工作。
四次挥手
流程
- 第一次挥手:主动关闭方(通常是客户端)向对方发送一个带有 FIN(结束标志)标志的 TCP 报文段,表示自己已经没有数据要发送了,请求关闭连接。此时,主动关闭方进入 FIN_WAIT_1 状态。
- 第二次挥手:被动关闭方接收到 FIN 报文段后,会向主动关闭方发送一个带有 ACK 标志的 TCP 报文段,确认号为主动关闭方的序列号加 1,表示已经收到了关闭请求。此时,被动关闭方进入 CLOSE_WAIT 状态,而主动关闭方收到 ACK 报文段后进入 FIN_WAIT_2 状态。
- 第三次挥手:被动关闭方在处理完所有数据后,会向主动关闭方发送一个带有 FIN 标志的 TCP 报文段,表示自己也没有数据要发送了,请求关闭连接。此时,被动关闭方进入 LAST_ACK 状态。
- 第四次挥手:主动关闭方接收到被动关闭方的 FIN 报文段后,会向被动关闭方发送一个带有 ACK 标志的 TCP 报文段,确认号为被动关闭方的序列号加 1。发送完这个 ACK 报文段后,主动关闭方进入 TIME_WAIT 状态,等待一段时间(通常是 2 倍的 MSL,即最长报文段寿命)后,确认对方已经收到 ACK 报文段,然后才真正关闭连接。被动关闭方收到 ACK 报文段后,立即关闭连接。
必要性
- 确保双方都能完成数据的发送和接收。在关闭连接之前,需要保证双方都已经将所有的数据发送完毕,并且对方已经成功接收。通过四次挥手,双方可以分别通知对方自己的数据发送情况,避免数据丢失。
- 实现可靠的连接关闭。在四次挥手中,每次挥手都有明确的确认机制,确保关闭请求能够被对方正确接收和处理。例如,主动关闭方发送 FIN 请求后,需要等待被动关闭方的 ACK 确认,以确保对方已经知道要关闭连接。
- 处理半关闭状态。在 TCP 连接中,允许一方先关闭自己的发送方向,而仍然保持接收方向的连接,这就是半关闭状态。四次挥手能够很好地处理这种情况,使得双方可以在不同的时间关闭各自的发送和接收通道,更加灵活地控制连接的关闭过程。
- 避免数据丢失和重复。TIME_WAIT 状态的存在是为了确保最后一个 ACK 报文段能够被对方成功接收。如果主动关闭方在发送完 ACK 报文段后立即关闭连接,而被动关闭方没有收到 ACK 报文段,那么被动关闭方可能会重新发送 FIN 报文段,而此时主动关闭方已经关闭了连接,就会导致数据丢失。通过等待一段时间,主动关闭方可以确保被动关闭方能够收到 ACK 报文段,从而避免这种情况的发生。同时,TIME_WAIT 状态也可以避免旧的连接请求报文段在网络中滞留,从而防止它们干扰新的连接。
3:图示
(3)三次握手
作用:在通信两端之间建立一个可靠的连接通道,连接建立后双方就可以进行数据通信。
简单版
可以理解为两个人Client和Server打电话为了确保都能听到而进行正式讲话前的测试。
- Client:喂喂喂,听得到不
- Server :我听到了,你听到我说话不?
- Client:听得到。我给你说个事儿…
经过以上三个步骤可以确认双方都在线,可以开始说正事了(发数据)
正式版
- 第一次握手:客户端发送tcp报文,并将syn置于1,初始序列号X。syn用于表示该次报文是用于连接使用。
- 第二次握手:服务器接受后发送连接确认**(Syn=1,ACK=X+1,Seq=Y)**报文,并给定服务器序列号Y
- 第三次握手:客户端收到确认报文后,也发送一个服务器确认报文**(ACK=Y+1),也就是将服务器序列号置为Y+1**,该报文syn不为1,避免认为是第一步的连接报文。
tips:
- Y+1和X+2就是后续发送数据的序列号。同时序列号大小跟发送数据的字节量有关。例如:建立链接后,客户端发送了100个字节的数据。假设连接之前序列号就是1(客户端的序列号),发送一个字节序列号就要加1,请问,发送100个字节数据之后,下一次客户端发送数据的序列号是多少?103
- 序列号本身是一个32位的数字。
(4)四次挥手
tcp连接结束正常过程会经历四次报文的确认,双方才会正确的关闭链接。
简单版
可以理解为两个人Client和Server电话打完为了确保双方话都说完了而进行的结束测试。
1.Client:我这边说完了,你说完了吗?
2 (Client聆听中)Server : 你讲完了?等等,我还有最后几句话
3Server : 我讲完了,挂了阿
4.Client:要得。挂了。
正式版:
- 第一次挥手:当某一端A发完数据后,想要关闭连接就发送一个FIN报文给对方B,表示我没有数据发送了,但端A可以继续接受数据。并端A进入最终等待1状态(FIN_wait 1)
- 第二次挥手:当端B接收到FIN报文后会立即发一个 ACK报文给A,表示收到,但我数据还没发完。端B进入CLOSE_WAIT状态(等待关闭),端A收到ACK后,转为最终等待2(FIN_WAIT2)状态。
- 第三次挥手:当端B数据发送完毕后,也会发送一个FIN报文给端A,表示数据已经发送完毕。端B进入最后确认(LAST_ACK)状态,端A进入TIME_WAIT状态
- 第四次挥手:当A收到B发 的ack报文后,会发送最终ack报文给端B,在一定时间(2MSL:2次数据最大生存时间)后,没有意外端A会关闭连接,端B收到最终报文后也会关闭连接,进入CLOSED状态。那么整个tcp链接就结束了。
注意:但有一点,如果端B没有收到最终的ACK,它会重传FIN报文,而在TIME_WAIT状态的主机A就可以捕获这个重传的FIN段,并再次发送ACK确认。
流程
从报文角度上解析
- 客户端先发一个FIN报文给服务器端(表示客户端数据发送完毕),服务器接受之后立即发送一个确认报文给客户端(表示我已接受,当我这边数据发送完毕时,还会发送报文)。当服务器数据发送完毕后发送一个结束报文(FIN=1)给客户端(表示数据已经发送完毕)。客户端接受服务器的fin报文后会发送最终确认报文给服务器。服务器接受报文后就会关闭。而客户端在等待一段时间后,没有新的报文接受就会自动关闭。
从状态的角度解析流程
- 客户端发送结束报文后进入FIN_WAIT1状态。服务器接受fin报文后进行close-wait状态并发送确认报文。
- 客户端接受服务器确认报文后进入FIN_WAIT2状态。
- 服务器发送FIN报文后服务器进入(LAST_ACK)状态。
- 客户端接受fin报文后并发送最终确认报文给服务器,同时客户端进入时间等待阶段(TIME_WAIT)状态。
- 服务器接受到最终报文后进入closed状态,即关闭了。
- 客户端在等待一段时间之后自动进入closed状态。
三-一、网络套接字编程总结
├─ 套接字(Socket)
│ ├─ 网络通信端点,由 IP + 端口唯一标识
│ ├─ 类型:
│ │ ├─ 流式套接字(SOCK_STREAM) → 基于 TCP,可靠、面向连接
│ │ └─ 数据报套接字(SOCK_DGRAM) → 基于 UDP,不可靠、无连接
│
├─ 地址结构
│ ├─ 通用结构:struct sockaddr(所有协议族共用)
│ ├─ IPv4 专用:struct sockaddr_in
│ │ ├─ sin_family
→ 地址族(AF_INET
表示 IPv4)
│ │ ├─ sin_port
→ 端口号(需用 htons()
转换字节序)
│ │ ├─ sin_addr
→ IP 地址(嵌套结构体 struct in_addr
)
│ │ │ ├─ s_addr
→ 32 位无符号整数(用 inet_addr()
转换点分十进制)
│ │ │ └─ INADDR_ANY
→ 特殊值,表示监听所有可用 IP
│ │ └─ sin_zero
→ 填充字节,确保与 sockaddr
大小一致
│ └─ IPv6 专用:struct sockaddr_in6(类似 sockaddr_in
,但适配 IPv6)
│
├─ 核心函数
│ ├─ 创建套接字 → socket(AF_INET, SOCK_STREAM, 0)
│ ├─ 绑定地址 → bind(sockfd, (struct sockaddr*)&server, sizeof(server))
│ ├─ 监听连接(TCP 专用) → listen(sockfd, 5)
│ ├─ 接受连接(TCP 专用) → accept(sockfd, &client_addr, &addrlen)
│ ├─ 发起连接(客户端) → connect(sockfd, (struct sockaddr*)&server, sizeof(server))
│ └─ 数据收发
│ ├─ TCP → send()
/recv()
或 write()
/read()
│ └─ UDP → sendto()
/recvfrom()
(需指定目标地址)
│
├─ TCP 套接字编程流程
│ ├─ 服务器端:
│ │ 1. socket()
→ 2. bind()
→ 3. listen()
→ 4. accept()
→ 5. recv()
/send()
→ 6. close()
│ └─ 客户端:
│ 1. socket()
→ 2. connect()
→ 3. send()
/recv()
→ 4. close()
│
└─ UDP 套接字编程流程
├─ 服务器端:
│ 1. socket()
→ 2. bind()
→ 3. recvfrom()
/sendto()
→ 4. close()
└─ 客户端:
socket()
→ 2.sendto()
/recvfrom()
→ 3.close()
关键细节
- 字节序转换
htons()
:将主机字节序转为网络字节序(用于端口号)inet_addr()
:将点分十进制 IP 转为网络字节序整数
- 地址结构强制转换
- 所有系统函数(如
bind
、connect
)要求传入struct sockaddr*
类型 - 实际使用时需将
struct sockaddr_in*
强制转换为struct sockaddr*
- 所有系统函数(如
- UDP 与 TCP 的核心区别
- UDP 无需
listen()
、connect()
,直接收发数据(无连接) - TCP 需三次握手建立连接,数据可靠有序
- UDP 无需
-
四:UDP编程
A:UDP协议
1:协议概念
UDP(用户数据报协议,User Datagram Protocol)是一个简单的网络通信协议,属于互联网协议簇中的一部分。与TCP(传输控制协议)相比,UDP是一种无连接的协议,这意味着在数据发送之前,发送者和接收者之间不需要建立连接,也就意味着当我们使用udp协议进行传输数据时,就没有类似tcp链接的机制,无法保证数据的传输准确性和可达性。即有可能会丢失数据。
udp同tcp一样,作用于传输层,针对两个设备或程序提供端到端之间的数据通信。在实际网路编程中是二选一。
2:udp协议特点
- 无连接:发送数据之前没有跟对方的一个连接过程。即不管另一端的状态,数据直接发
- 不可靠:发送的数据不会管另一方是否正常,也不会有错误检测,数据丢失重发等机制。
- 高效:没有tcp等链接过程的开销,以及数据报文的空间开销较小,发送效率很高
- 应用:特别适合实时性的场景。比如直播、视频通话、语音通话等。
B:UDP编程流程
udp编程也是跟tcp一样,分为客户端和服务器端两个部分,大致过程和tcp编程是类似的,具体如下图所示:
(1)客户端过程
通过socket创建一个套接字
利用
sendto
函数声明要发送的数据以及要接受数据的服务器端信息如果需要接受数据,调用
recvfrom
来接受服务器端发来的数据,一样在recvfrom
中声明数据的来源是谁。不需要发送时关闭客户端套接字
(2)服务器端过程
- 通过socket创建一个套接字
- 通过bind来绑定一个端口。
- 通过调用recvfrom可以来接受客户端发来的数据。
- 可以调用sendto 来将数据发送给指定的客户端。
- 不需要发送和接受数据后关闭套接字
C:UDP相关函数
以下函数皆包含在于
sys/socket.h
头文件中。1:sendto
概念
sendto
函数用于UDP协议的套接字发送数据。它是一个用于无连接的数据发送的系统调用,可将数据发送至指定的目标地址。语法
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。dest_addr
:指向包含目的地地址的结构体的指针,通常结构体类型为sockaddr
。addrlen
:目的地地址结构体的大小。
返回值
- 返回已发送的字节数,失败时返回-1,并设置errno以指示错误。
2:recvfrom
概念
recvfrom
函数用于UDP协议的套接字接收数据。它可以从套接字中接收数据,并获取数据发送方的地址语法
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
参数
sockfd
:接收数据的套接字文件描述符。buf
:指向用于接收数据的缓冲区的指针。len
:缓冲区的大小,以字节为单位。flags
:设置调用执行方式的标志位,通常设置为0,表示使用默认的方式来发送,意味着会至少接受一个字节,不然会阻塞程序执行,直到接受到数据。src_addr
:(可选)指向存储源地址的结构体的指针。addrlen
:(可选)指向源地址结构体大小的指针。如果不关心源地址,可以分别将src_addr和addrlen设置为NULL
返回值
- 返回接收到的字节数,如果连接已经关闭,返回0,失败时返回-1,并设置errno以指示错误
D:UDP编程实例
进行一次互相数据发送和接受的基本使用例子:
1:服务端
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #define PORT 8081 #define BUFFER_SIZE 1024 int main(int argc, char *argv[]) { int server_fd; struct sockaddr_in server_addr = {0}; struct sockaddr_in client_addr = {0}; socklen_t client_addr_len = sizeof(client_addr); // 1. 创建 UDP 套接字 if ((server_fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) { perror("socket() 创建失败"); exit(EXIT_FAILURE); } // 2. 设置服务端地址 //memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = INADDR_ANY; server_addr.sin_port = htons(PORT); // 3. 绑定地址和端口 if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { perror("bind() 失败"); close(server_fd); exit(EXIT_FAILURE); } printf("UDP 服务端已启动,等待客户端连接...\n"); // 4. 接收客户端消息 char buffer[BUFFER_SIZE]; int len = recvfrom(server_fd, buffer, sizeof(buffer), 0, (struct sockaddr *)&client_addr, &client_addr_len); if (len < 0) { perror("recvfrom() 失败"); close(server_fd); exit(EXIT_FAILURE); } buffer[len] = '\0'; // 添加字符串终止符 printf("收到客户端消息: %s\n", buffer); // 5. 向客户端发送消息 const char *message = "Hello, UDP Client!"; if (sendto(server_fd, message, strlen(message), 0, (struct sockaddr *)&client_addr, client_addr_len) < 0) { perror("sendto() 失败"); close(server_fd); exit(EXIT_FAILURE); } printf("向客户端发送消息: %s\n", message); // 6. 关闭套接字 close(server_fd); return 0; }
2:客户端
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #define SERVER_IP "127.0.0.1" #define PORT 8081 #define BUFFER_SIZE 1024 int main() { int client_fd; struct sockaddr_in server_addr={0}; socklen_t server_addr_len = sizeof(server_addr); // 1. 创建 UDP 套接字 if ((client_fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) { perror("socket() 创建失败"); exit(EXIT_FAILURE); } // 2. 设置服务器地址 //memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); server_addr.sin_port = htons(PORT); // 3. 向服务端发送消息 const char *message = "Hello, UDP Server!"; if (sendto(client_fd, message, strlen(message), 0, (struct sockaddr *)&server_addr, server_addr_len) < 0) { perror("sendto() 失败"); close(client_fd); exit(EXIT_FAILURE); } printf("向服务端发送消息: %s\n", message); // 4. 接收服务端消息 char buffer[BUFFER_SIZE]; ssize_t len = recvfrom(client_fd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&server_addr, &server_addr_len); if (len < 0) { perror("recvfrom() 失败"); close(client_fd); exit(EXIT_FAILURE); } buffer[len] = '\0'; printf("收到服务端消息: %s\n", buffer); // 5. 关闭套接字 close(client_fd); return 0; }
E:实现多次通话:
服务端:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #include <stdbool.h> #define PORT 8081 #define BUFFER_SIZE 1024 int main(int argc, char *argv[]) { int server_fd; struct sockaddr_in server_addr = {0}; struct sockaddr_in client_addr = {0}; socklen_t client_addr_len = sizeof(client_addr); // 1. 创建 UDP 套接字 if ((server_fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) { perror("socket() 创建失败"); exit(EXIT_FAILURE); } // 2. 设置服务端地址 //memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = INADDR_ANY; server_addr.sin_port = htons(PORT); // 3. 绑定地址和端口 if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { perror("bind() 失败"); close(server_fd); exit(EXIT_FAILURE); } printf("UDP 服务端已启动,等待客户端连接...\n"); // 接收和发送是多次的,应该在循环里: char buffer[BUFFER_SIZE]; char message[BUFFER_SIZE]; while(true){ // 4. 接收客户端消息 int len = recvfrom(server_fd, buffer, sizeof(buffer), 0, (struct sockaddr *)&client_addr, &client_addr_len); if (len < 0) { perror("recvfrom() 失败"); close(server_fd); exit(EXIT_FAILURE); } buffer[len] = '\0'; // 添加字符串终止符 printf("收到客户端消息: %s\n", buffer); // 5. 向客户端发送消息 puts("发给客户端的消息:"); //scanf("%s", message); 不能有空格 fgets(message, BUFFER_SIZE, stdin); // strcspn()函数,返回缓冲区里的第一个\n的位置 message[strcspn(message, "\n")] = '\0'; // 把换行符换成字符结束符 if (sendto(server_fd, message, strlen(message), 0, (struct sockaddr *)&client_addr, client_addr_len) < 0) { perror("sendto() 失败"); close(server_fd); exit(EXIT_FAILURE); } // 检查退出条件 if (strcmp(message, "exit") == 0) { printf("服务端退出,服务端关闭。\n"); break; } printf("-----向客户端发送消息完成-----\n"); } // 6. 关闭套接字 close(server_fd); return 0; }
客户端:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #include <stdbool.h> #define SERVER_IP "127.0.0.1" #define PORT 8081 #define BUFFER_SIZE 1024 int main() { int client_fd; struct sockaddr_in server_addr={0}; socklen_t server_addr_len = sizeof(server_addr); // 1. 创建 UDP 套接字 if ((client_fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) { perror("socket() 创建失败"); exit(EXIT_FAILURE); } // 2. 设置服务器地址 //memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); server_addr.sin_port = htons(PORT); // 发送和接收是多次的,所以放循环里面: char buffer[BUFFER_SIZE]; char message[BUFFER_SIZE]; while(true){ puts("请输入你要说的话:"); //scanf("%s", message); 不能有空格 fgets(message, BUFFER_SIZE, stdin); // strcspn()函数,返回缓冲区里的第一个\n的位置 message[strcspn(message, "\n")] = '\0'; // 把换行符换成字符结束符 // 检查退出条件 if (strcmp(message, "exit") == 0) { printf("客户端退出。\n"); break; } // 3. 向服务端发送消息 if (sendto(client_fd, message, strlen(message), 0, (struct sockaddr *)&server_addr, server_addr_len) < 0) { perror("sendto() 失败"); close(client_fd); exit(EXIT_FAILURE); } printf("-----向服务端发送消息完成-----\n"); // 4. 接收服务端消息 ssize_t len = recvfrom(client_fd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&server_addr, &server_addr_len); if (len < 0) { perror("recvfrom() 失败"); close(client_fd); exit(EXIT_FAILURE); } buffer[len] = '\0'; // 检查退出条件: 如何服务端退出了,客户端也退出 if (strcmp(buffer, "exit") == 0) { printf("客户端退出。\n"); break; } printf("收到服务端消息: %s\n", buffer); } // 5. 关闭套接字 close(client_fd); return 0; }
F:实现多客户端:
实现一个服务端与多个客户端通信,UDP 本身是无连接的协议,因此服务端不需要为每个客户端维护单独的连接。服务端只需要接收来自不同客户端的消息,并根据客户端的地址(
client_addr
)进行回复即可。这里我们加上打印客户端地址的功能即可(可以知道是谁发送的消息。。。)。
服务端支持多客户端:
- 服务端通过
recvfrom
接收消息时,会获取客户端的地址信息(client_addr
)。 - 服务端根据客户端的地址信息(
client_addr
)回复消息,使用sendto
发送到正确的客户端。 - 所以原来的代码根本不用修改,就能支持多客户端
打印客户端信息:
- 使用
inet_ntop
将客户端的 IP 地址从二进制格式转换为字符串格式。 - 使用
ntohs
将客户端的端口号从网络字节序转换为主机字节序。
退出条件:
- 如果客户端发送
exit
,服务端会打印退出信息,但不关闭服务,而继续等待其他客户端消息。
服务端:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #include <stdbool.h> #define PORT 8081 #define BUFFER_SIZE 1024 int main(int argc, char *argv[]) { int server_fd; struct sockaddr_in server_addr = {0}; struct sockaddr_in client_addr = {0}; socklen_t client_addr_len = sizeof(client_addr); // 1. 创建 UDP 套接字 if ((server_fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) { perror("socket() 创建失败"); exit(EXIT_FAILURE); } // 2. 设置服务端地址 //memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = INADDR_ANY; server_addr.sin_port = htons(PORT); // 3. 绑定地址和端口 if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { perror("bind() 失败"); close(server_fd); exit(EXIT_FAILURE); } printf("UDP 服务端已启动,等待客户端连接...\n"); // 接收和发送是多次的,应该在循环里: char buffer[BUFFER_SIZE]; char message[BUFFER_SIZE]; while(true){ // 4. 接收客户端消息 int len = recvfrom(server_fd, buffer, sizeof(buffer), 0, (struct sockaddr *)&client_addr, &client_addr_len); if (len < 0) { perror("recvfrom() 失败"); close(server_fd); exit(EXIT_FAILURE); } buffer[len] = '\0'; // 添加字符串终止符 // 打印客户端信息 ======================================== char client_ip[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN); printf("收到来自客户端 %s:%d 的消息: %s\n", client_ip, ntohs(client_addr.sin_port), buffer); // 检查退出条件 if (strcmp(buffer, "exit") == 0) { printf("客户端 %s:%d 请求退出。\n", client_ip, ntohs(client_addr.sin_port)); continue; // 继续等待其他客户端的消息 } // 5. 向客户端发送消息 puts("发给客户端的消息:"); //scanf("%s", message); 不能有空格 fgets(message, BUFFER_SIZE, stdin); // strcspn()函数,返回缓冲区里的第一个\n的位置 message[strcspn(message, "\n")] = '\0'; // 把换行符换成字符结束符 if (sendto(server_fd, message, strlen(message), 0, (struct sockaddr *)&client_addr, client_addr_len) < 0) { perror("sendto() 失败"); close(server_fd); exit(EXIT_FAILURE); } printf("向客户端 %s:%d 发送消息: %s\n", client_ip, ntohs(client_addr.sin_port), message); // 检查退出条件 if (strcmp(message, "exit") == 0) { printf("服务端退出,服务端关闭。\n"); break; } printf("-----向客户端发送消息完成-----\n"); } // 6. 关闭套接字 close(server_fd); return 0; }
客户端:
客户端代码不需要修改,因为 UDP 是无连接的,多个客户端可以同时向服务端发送消息。
G:UDP和TCP的区别
UDP和TCP都是在传输层用于实现端和端之间的数据通信,主要有以下区别:
(1)连接性:
- tcp:面向连接的协议,这意味着在数据传输开始之前,必须首先建立连接
- udp:无连接的协议,它不需要在数据传输前建立连接,数据包(称为数据报)可以随时发送到另一个设备
(2)可靠性:
- tcp:TCP 提供可靠的服务,它确保数据正确无误地从源传输到目标
- udp:UDP 不保证数据的可靠传输,没有建立机制来确保数据包的顺序或检测丢失的数据包,也没有重传机制
(3)效率方面:
- tcp:TCP 由于其重传、错误检测和顺序保证机制,相对于UDP来说通常速度较慢
- udp:UDP 由于缺乏复杂的错误检查和响应机制,使得它在网络通信中有更少的延迟
(4)数据流控制
- tcp:提供流量控制和拥塞控制机制,这有助于网络在高负载时维持稳定性
- udp:则没有内建的流控制或拥塞控制,发送方的数据发送速率不会根据网络情况自动调整
(5)应用场景:
- tcp:通常用于需要高可靠性的应用,如网页浏览、文件传输、电子邮件等。
- udp:通常用于流媒体视频或音频传输、在线游戏、广播通信等场景,要求实时性较高。实际会配合rtp等应用层协议一起使用,保证实时性的同时兼顾数据的顺序和丢失重发机制。
五、通信的过程
(1)数据的传输过程
(2)报文举例
- 序列号:记录发送数据的序号。比如第一字节的序号为1,第101个字节的序号为101。序号都是32位的
- 确认号:接受对方数据的序号,序号本身是32位
六、补充端口号
- 概念:端口号是操作系统为了管理各个应用程序和外网进行数据通信的机制。如果一个程序需要访问网络,那么该程序就必须要绑定一个端口号,才能和外界进行数据交互。
- 端口号本质上是一个数字,是操作系统已启动就会开启。每个端口号都可以为一个应用程序提供网络通信的数据通道。
- 范围:1~65535. 尽量网络编程使用:1000及以上,建议特殊数字,一般程序不会用
七、并发服务器实现
- 概念:并发服务器是指能够同时处理多个客户端连接的服务器(借助多线程)
- 流程
客户端:
- 调用socket()创建一个套接字,并指定ip地址和使用协议,得到一个套接字描述符
- 调用Connect()尝试连接一个服务器
- 链接成功之后使用read()或write()进行接受服务器返回的数据或发送数据给服务器
- 不需要链接之后调用close()关闭套接字,释放套接字所占用的资源。
服务器端:
调用socket()创建一个套接字,并指定ip地址和使用协议,得到一个套接字描述符
调用bind()绑定一个端口号
调用listen用于监听绑定的端口
利用while循环调用accept来接受某个客户端的链接,连接成功后创建一个线程来处理该客户端的数据处理。这一步重复执行
不需要链接之后调用close()关闭套接字,释放套接字所占用的资源。
八:CS架构实现:
服务端,除了可以响应字符串到客户端,还可以响应其它类型的数据到客户端:
服务端:
#include <stdio.h> #include <stdbool.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <unistd.h> // read(),write()和close()等api #include <arpa/inet.h> // 提供字节序转换(比如主机字节序和网络字节序的转换)和地址转换的函数 #include <sys/socket.h> // socket(),connect(),accept(),bind(),listen()和shutdown() #include <netinet/in.h> // 用于地址族,sockaddr, sockaddr_in 结构体和 IP 地址定义 typedef struct Student { int id; char name[32]; int age; }Student; int main(int argc, char *argv[]) { // 1: 创建服务端套接字: int server_fd = socket(AF_INET, SOCK_STREAM, 0); // 第三个参数:0代表自动选择 if(server_fd < 0){ perror("socket() error!"); exit(EXIT_FAILURE); } // 2: 绑定端口: // 2-1: 设置端口和地址信息: struct sockaddr_in server_addr = {0}; server_addr.sin_family = AF_INET; //ipv4 server_addr.sin_port = htons(8081); //设置端口和将主机字节序(host byte order)的 16 位整数转换为网络字节序(network byte order) server_addr.sin_addr.s_addr = INADDR_ANY ; // 代表接收任意IPv4地址的信息 // 2-2: 绑定 if(bind( server_fd , (struct sockaddr *)&server_addr , sizeof(server_addr ) ) == -1){ perror("bind () error!"); close(server_fd); exit(EXIT_FAILURE); } // 3: 监听: if(listen( server_fd , 1) == -1){ perror("listen() error!"); close(server_fd); exit(EXIT_FAILURE); } puts("服务器正在监听8080端口......"); // 4: 接受客户端的连接: // 4-1: 准备客户端的地址结构体: struct sockaddr_in client_addr = {0}; socklen_t client_addr_length = sizeof(client_addr); // 如果没有客户端连接,这里会阻塞。。 int client_fd = accept( server_fd , (struct sockaddr *)&client_addr , &client_addr_length ); if(client_fd == -1){ perror("accept() error!"); close(server_fd); exit(EXIT_FAILURE); } puts("有客户端连接上来了!"); // 5: 进行读和写: char buf[1024]; // 缓冲区大小 ssize_t count; // 读取的字节数 Student ss[4]; // 测试数据:结构体数组,模拟从数据库查出的数据 Student s1 = {100, "张三", 23}; Student s2 = {101, "rose", 26}; Student s3 = {102, "李强", 21}; Student s4 = {103, "小丽", 22}; ss[0] = s1; ss[1] = s2; ss[2] = s3; ss[3] = s4; // ---- 多次的读和写的过程 ------------------- while(true){ if( (count = read( client_fd , buf , sizeof(buf) - 1 )) > 0 ){ buf[count] = '\0'; printf("从客户端过来的消息:%s \n", buf ); // 回复客户端的请求:------------------- // 1: 先发送数组的长度: int array_length = sizeof(ss) / sizeof(ss[0]); // 数组长度 if (write(client_fd, &array_length, sizeof(array_length)) < 0) { perror("发送数组长度失败!"); //close(client_fd); //close(server_fd); exit(EXIT_FAILURE); } // 2: 再写出数组数据 if(write( client_fd, ss , sizeof(ss) ) < 0 ){ perror("回复信息失败!"); //close(client_fd); //close(server_fd); break; } }else if (count == 0){ puts("客户端已经关闭了。"); break; }else { perror("读取数据错误"); break; } } // 6: 关闭资源: close(client_fd); close(server_fd); puts("------- server closed -------"); return 0; }
客户端:
#include <stdio.h> #include <stdbool.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <unistd.h> // read(),write()和close()等api #include <arpa/inet.h> // 提供字节序转换(比如主机字节序和网络字节序的转换)和地址转换的函数 #include <sys/socket.h> // socket(),connect(),accept(),bind(),listen()和shutdown() #include <netinet/in.h> // 用于地址族,sockaddr, sockaddr_in 结构体和 IP 地址定义 typedef struct Student { int id; char name[32]; int age; } Student; int main(int argc, char *argv[]) { // 1: 创建客户端套接字: int client_fd = socket(AF_INET, SOCK_STREAM, 0); // 第三个参数:0代表自动选择 if(client_fd < 0){ perror("socket() error!"); exit(EXIT_FAILURE); } // 2: 连接到服务端: // 2-1: 先准备地址结构体: struct sockaddr_in server_addr = {0}; server_addr.sin_family = AF_INET; //ipv4 server_addr.sin_port = htons(8081); //设置端口和将主机字节序的 16 位整数转换为网络字节序 server_addr.sin_addr.s_addr = inet_addr("127.0.0.1") ; // 代表连接到指定地址的服务器 if(connect( client_fd , (struct sockaddr *)&server_addr , sizeof(server_addr) ) < 0) { perror("连接失败"); close(client_fd); exit(EXIT_FAILURE); } puts("连接服务器成功!"); // 3: -------------------------------------------- char str[128]; while(true){ // 3-1: 发送信息给服务器: puts("你说:"); scanf("%s", str); // 判断是否退出: if(strcmp("exit", str) == 0){ break; } int count = write( client_fd, str , strlen(str) ); printf("向服务器发送:%d 个字节的数据。-----------\n", count ); // 3-2: 接收服务端回复的消息: // 1: 先接收数组的长度: int array_length; if (read(client_fd, &array_length, sizeof(array_length)) <= 0) { perror("接收数组长度失败!"); close(client_fd); exit(EXIT_FAILURE); } // 2:再根据数组的长度,动态分配内存 Student *ss = malloc(sizeof(Student) * array_length); if (ss == NULL) { perror("内存分配失败!"); close(client_fd); exit(EXIT_FAILURE); } // 3: 再接收服务端传过来的数组数据 int num; if( (num = read( client_fd , ss , sizeof(Student) * array_length )) > 0){ printf("服户端回复的数据如下 :\n" ); for(int i = 0; i < array_length ; i ++){ printf("学生的id是:%d , 名字是:%s, 年龄是:%d \n", ss[i].id, ss[i].name, ss[i].age); } // 使用完数据,得回收内存 free(ss); }else if(num == 0){ puts("服务器已经关闭"); break; }else { puts("读取响应数据出错。"); break; } } // 4: 关闭资源 close(client_fd); puts("======= client closed ======="); return 0; }
九、群聊服务器实现
- 目的:可以多个客户端连接进服务器,并可以发消息。当其中一个客户端发消息时,其他客户端可以接受该客户发来的消息。
- 思路:
- 服务器每接收一个客户端连接时,将客户端结构体保存到数组中,定义一个函数,函数用于遍历结构体数组,依次的发送数据。该函数就是用于群发功能。每个客户端发送数据时就调用该群发函数,将昵称、时间、群发信息发送给群发函数并进行群发。
- 客户端:需要开启一个新的线程用于接受群发的信息。main函数用于发送信息给客户端。
示例代码:
服务端:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <pthread.h> #define PORT 8080 #define MAX_CLIENTS 100 #define BUFFER_SIZE 1024 int client_sockets[MAX_CLIENTS]; int client_count = 0; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 广播消息给所有客户端 void broadcast_message(char *message, int sender_socket) { pthread_mutex_lock(&mutex); for (int i = 0; i < client_count; i++) { if (client_sockets[i] != sender_socket) { if (send(client_sockets[i], message, strlen(message), 0) < 0) { perror("发送失败"); } } } pthread_mutex_unlock(&mutex); } // 处理客户端连接 void *handle_client(void *arg) { int client_socket = *(int *)arg; char buffer[BUFFER_SIZE]; while (1) { int len = recv(client_socket, buffer, sizeof(buffer), 0); if (len <= 0) { // 客户端断开连接 pthread_mutex_lock(&mutex); for (int i = 0; i < client_count; i++) { if (client_sockets[i] == client_socket) { // 从客户端列表中移除 client_sockets[i] = client_sockets[client_count - 1]; client_count--; break; } } pthread_mutex_unlock(&mutex); printf("客户端 %d 断开连接\n", client_socket); close(client_socket); break; } buffer[len] = '\0'; printf("收到消息: %s\n", buffer); broadcast_message(buffer, client_socket); } return NULL; } int main() { int server_socket; struct sockaddr_in server_addr; // 创建 TCP 套接字 if ((server_socket = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("socket() 失败"); exit(EXIT_FAILURE); } // 设置服务器地址 server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = INADDR_ANY; server_addr.sin_port = htons(PORT); // 绑定地址和端口 if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { perror("bind() 失败"); close(server_socket); exit(EXIT_FAILURE); } // 监听连接 if (listen(server_socket, 5) < 0) { perror("listen() 失败"); close(server_socket); exit(EXIT_FAILURE); } printf("服务器已启动,等待客户端连接...\n"); while (1) { struct sockaddr_in client_addr; socklen_t client_addr_len = sizeof(client_addr); int client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &client_addr_len); if (client_socket < 0) { perror("accept() 失败"); continue; } // 将客户端套接字加入列表 pthread_mutex_lock(&mutex); client_sockets[client_count++] = client_socket; pthread_mutex_unlock(&mutex); // 创建线程处理客户端 pthread_t thread; pthread_create(&thread, NULL, handle_client, &client_socket); pthread_detach(thread); printf("新客户端连接: %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); } close(server_socket); return 0; }
客户端:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <pthread.h> #define SERVER_IP "127.0.0.1" #define PORT 8080 #define BUFFER_SIZE 1024 // 接收服务器消息 void *receive_messages(void *arg) { int client_socket = *(int *)arg; char buffer[BUFFER_SIZE]; while (1) { int len = recv(client_socket, buffer, sizeof(buffer), 0); if (len <= 0) { printf("与服务器断开连接\n"); exit(EXIT_FAILURE); } buffer[len] = '\0'; printf("%s\n", buffer); } return NULL; } int main() { int client_socket; struct sockaddr_in server_addr; // 创建 TCP 套接字 if ((client_socket = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("socket() 失败"); exit(EXIT_FAILURE); } // 设置服务器地址 server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); server_addr.sin_port = htons(PORT); // 连接服务器 if (connect(client_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { perror("connect() 失败"); close(client_socket); exit(EXIT_FAILURE); } printf("已连接到服务器\n"); // 创建线程接收消息 pthread_t thread; pthread_create(&thread, NULL, receive_messages, &client_socket); // 发送消息 char buffer[BUFFER_SIZE]; while (1) { fgets(buffer, BUFFER_SIZE, stdin); buffer[strcspn(buffer, "\n")] = '\0'; // 去掉换行符 if (send(client_socket, buffer, strlen(buffer), 0) < 0) { perror("发送失败"); break; } } close(client_socket); return 0; }
十:字节序补充
1:网络字节序强制使用大端(Big-Endian)
发送前调用
htonl()
/htons()
将主机字节序转为网络字节序htonl()
:处理 4 字节数据(如int32_t
、IPv4地址)。htons()
:处理 2 字节数据(如端口号、int16_t
)。
接收后调用
ntohl()
/ntohs()
将网络字节序转回主机字节序- ntohl() Network to Host Long(32位逆转换)
- ntohs() Network to Host Short(16位逆转换)
// 服务端发送数据示例 uint32_t num = 0x12345678; uint32_t net_num = htonl(num); send(client_socket, &net_num, sizeof(net_num), 0); // 客户端接收数据示例 uint32_t recv_num; recv(server_socket, &recv_num, sizeof(recv_num), 0); uint32_t host_num = ntohl(recv_num);
总结
htonl()
:处理 4 字节数据(如int32_t
、IPv4地址)。htons()
:处理 2 字节数据(如端口号、int16_t
)。- 强制规则:只要传输二进制数值,必须显式调用对应的函数,无论当前主机是否为大端!
2: 结构体的特殊处理
- 避免直接发送结构体(不同平台对齐方式可能不同)
- 推荐逐个字段序列化:
struct MyData { int32_t a; float b; }; // 序列化 int32_t net_a = htonl(data.a); float net_b = htonf(data.b); // 假设存在htonf() send(socket, &net_a, sizeof(net_a), 0); send(socket, &net_b, sizeof(net_b), 0); // 反序列化同理
3: 使用序列化库
- Protocol Buffers、MessagePack 等库自动处理字节序
- 示例(伪代码)
// 定义Protobuf消息 message Data { int32 value = 1; } // 序列化 Data data; data.set_value(1234); std::string serialized = data.SerializeToString(); send(socket, serialized.data(), serialized.size(), 0); // 反序列化 Data parsed_data; parsed_data.ParseFromString(received_data);
4: 错误场景
- 直接发送本地结构体
5:总结
- 必须处理:若传输二进制数据(数值、结构体等)
- 无需处理:若使用纯文本协议
- 最佳实践:优先使用网络字节序转换函数(
htonl
/ntohl
)或成熟的序列化库
6:数据发送时注意
无论当前主机是大端(Big-Endian)还是小端(Little-Endian),只要传输二进制数值(如整数、浮点数等),就必须显式调用
htonl()
/htons()
进行字节序转换。为什么?
- 网络协议强制规定大端字节序
- 所有网络协议(如TCP/IP)定义字段(如端口号、IP地址、数据长度)均要求使用大端字节序。
- 即使你的主机是大端机器,也必须遵守协议规范,否则其他小端机器无法正确解析数据。
- 代码可移植性
- 假设你在小端机器(如x86架构)上开发时不转换,代码在大端机器(如PowerPC)上运行时必然出错。
- 显式转换确保代码在任何平台的行为一致。
- 未来兼容性
- 即使当前所有运行环境都是大端机器,未来若迁移到小端机器,未转换的代码会直接崩溃。
- 统一转换是防御性编程的最佳实践。
- 网络协议强制规定大端字节序
示例:
// 发送方:强制转换所有二进制数值 uint32_t num = 12345; uint32_t net_num = htonl(num); // 必须调用,无论主机字节序 send(socket, &net_num, sizeof(net_num), 0); // 接收方:必须逆转换 uint32_t recv_num; recv(socket, &recv_num, sizeof(recv_num), 0); uint32_t host_num = ntohl(recv_num); // 必须调用
强制转换规则的意义
场景 不转换的后果 强制转换后的行为 主机A(小端) → 主机B(大端) 数据错误(字节序相反) 数据正确(自动适配) 主机A(小端) → 主机B(小端) 数据错误(网络协议要求大端) 数据正确(协议层兼容) 主机A(大端) → 主机B(大端) 数据正确(但违反协议规范) 数据正确(符合协议规范) 总结
- 必须遵守:无论主机字节序,传输二进制数值必须调用
htonl()
/htons()
。 - 底层原理:网络协议强制大端,转换函数自动适配主机字节序。
- 核心价值:保证代码的 跨平台性、协议兼容性 和 未来可维护性。
- 必须遵守:无论主机字节序,传输二进制数值必须调用
tcp和udp网络套接字编程 - 树状图总结
1. 套接字(Socket)
- TCP 套接字(SOCK_STREAM)
- 面向连接(可靠传输)
- 三次握手建立连接
- 适用于 HTTP、FTP、SSH 等
- UDP 套接字(SOCK_DGRAM)
- 无连接(不可靠但高效)
- 适用于 DNS、视频流、游戏等
2. 地址结构(sockaddr)
通用套接字地址结构(
struct sockaddr
)struct sockaddr { sa_family_t sa_family; // 地址族(AF_INET, AF_INET6) char sa_data[14]; // 地址数据(IP + Port) };
IPv4 专用结构(
struct sockaddr_in
)struct sockaddr_in { sa_family_t sin_family; // AF_INET(IPv4) in_port_t sin_port; // 端口号(16位) struct in_addr sin_addr; // IP 地址(32位) char sin_zero[8]; // 填充(保持与 sockaddr 大小一致) };
sin_family
AF_INET
(IPv4)AF_INET6
(IPv6)
sin_port
htons(8080)
(主机字节序→网络字节序)
sin_addr
inet_addr("127.0.0.1")
(字符串→32位IP)INADDR_ANY
(绑定所有可用IP)
IPv6 专用结构(
struct sockaddr_in6
)struct sockaddr_in6 { sa_family_t sin6_family; // AF_INET6 in_port_t sin6_port; // 端口号 uint32_t sin6_flowinfo; // 流信息 struct in6_addr sin6_addr; // IPv6 地址 uint32_t sin6_scope_id; // 作用域ID };
3. 关键函数
TCP 流程
socket() → bind() → listen() → accept() → recv()/send() → close()
bind(sockfd, (struct sockaddr*)&server, sizeof(server))
- 绑定 IP 和端口
listen(sockfd, backlog)
- 设置监听队列大小(等待连接数)
accept(sockfd, (struct sockaddr*)&client, &client_len)
- 接受客户端连接
UDP 流程
socket() → bind() → recvfrom()/sendto() → close()
recvfrom(sockfd, buf, len, flags, (struct sockaddr*)&client, &client_len)
- 接收数据并获取客户端地址
sendto(sockfd, buf, len, flags, (struct sockaddr*)&client, client_len)
- 向指定地址发送数据
4. 字节序转换
htons()
(Host to Network Short,16位)htonl()
(Host to Network Long,32位)ntohs()
(Network to Host Short)ntohl()
(Network to Host Long)
5. IP 地址转换
inet_addr("192.168.1.1")
(字符串→32位整数)inet_ntoa(struct in_addr)
(32位整数→字符串)inet_pton(AF_INET, "192.168.1.1", &addr)
(字符串→二进制)inet_ntop(AF_INET, &addr, buf, INET_ADDRSTRLEN)
(二进制→字符串)
思维导图(简化版)
网络套接字编程
├── 套接字类型
│ ├── TCP(SOCK_STREAM)
│ └── UDP(SOCK_DGRAM)
├── 地址结构
│ ├── sockaddr(通用)
│ ├── sockaddr_in(IPv4)
│ │ ├── sin_family(AF_INET)
│ │ ├── sin_port(htons)
│ │ └── sin_addr(inet_addr/INADDR_ANY)
│ └── sockaddr_in6(IPv6)
├── 关键函数
│ ├── TCP:socket→bind→listen→accept→recv/send→close
│ └── UDP:socket→bind→recvfrom/sendto→close
└── 辅助函数
├── 字节序转换(htons, ntohs)
└── IP 转换(inet_addr, inet_ntoa, inet_pton, inet_ntop)
这样整理后,整个套接字编程的核心概念和流程就清晰了!