Linux应用(6)——网络通信/TCP/IP
借鉴博客:https://blog.csdn.net/2401_83603768/article/details/151687501;https://blog.csdn.net/sunyctf/article/details/128975665?ops_request_misc=%257B%2522request%255Fid%2522%253A%252233308df854b34cb7450824a35c61f627%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=33308df854b34cb7450824a35c61f627&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-128975665-null-null.142^v102^control&utm_term=TCP%2Fip%E5%8D%8F%E8%AE%AE&spm=1018.2226.3001.4187
一、简介
1.1 OSI七层模型
国际标准化组织( ISO)在 1978 年提出了“开放系统互联参考模型”,即著名的 OSI/RM 模型(Open System Interconnection/Reference Model)。它将计算机网络体系结构的通信协议划分为七层,自下而上依次为:物理层( Physics Layer)、数据链路层( Data Link Layer)、网络层( Network Layer)、传输层(Transport Layer)、会话层(Session Layer)、表示层( Presentation Layer)、应用层( Application Layer)。
1.2 TCP/IP四层模型
传输控制协议(Transmission Control Protocol)/互联网协议(Internet Protocol)是一种用于连接网络设备的协议族,广泛应用于互联网和局域网中。它提供了在不同类型的网络上进行通信的标准和方法。
TCP/IP协议包主要通过组包与解包,也就是说在应用程序发送数据的过程中,数据在每一层都会增加一些信息,这些信息用于和接收端同层次进行沟通,每一层所加信息的作用不同。
层次 | 发送方作用(封装) | 接收方作用(解封装) | 数据单位 | 关键地址 | 比喻 |
---|---|---|---|---|---|
应用层 | 生成原始数据,定义应用协议 | 将数据交给正确的应用程序 | 报文/数据流 | - | 写信和准备礼物 |
传输层 | 建立端到端连接,保证可靠性 | 重组数据段,保证顺序和完整性 | 段/数据报 | 端口号 | 选择快递公司,贴运单 |
网络层 | 逻辑寻址,选择最佳路径 | 根据IP地址进行最终交付 | 包 | IP地址 | 贴全球地址标签,路由 |
数据链路层 | 物理寻址,错误检测,帧传输 | 错误检测,物理寻址 | 帧 | MAC地址 | 贴本地运输单,装袋 |
物理层 | 将比特流转换为物理信号传输 | 将物理信号转换回比特流 | 比特 | - | 装上卡车,变成信号 |
二、 网络接口层
2.1 APR 协议
根据已知的IP地址,查询其对应的MAC地址;主机会检查自己的ARP缓存表,看是否有目的IP对应的MAC地址,如果没有,主机就会在局域网内广播一个 ARP请求包,局域网内所有主机都会收到这个广播,但只有目的IP 的主机会响应,目的IP的主机会向请求方单播一个ARP响应包,告诉请求方自己的MAC地址。
2.2 RAPR 协议
根据已知的MAC地址,查询其对应的IP地址
2.3 MPLS协议
在网络核心提供高速、可控的数据转发,将IP路由的灵活性和二层交换的速度结合起来
三、网络层
3.1 IP
①IP是网络层的核心协议,其主要作用是实现网络互连,将数据包从源主机跨越多个网络传输到目的主机,它是无连接、不可靠的数据报传输;
②IP协议使用IP地址来标识网络中的每个设备(主机和路由器)
③IP数据包头部包含源IP地址和目的IP地址。路由器根据目的IP地址和路由表为数据包选择路径(路由)
④八位的TTL字段。这个字段规定该数据包在穿过多少个路由之后才会被抛弃。某个IP数据包每穿过一个路由器,该数据包的TTL数值就会减少1,当该数据包的TTL成为零,它就会被自动抛弃
3.2 ICMP
ICMP是IP协议的辅助协议,用于在IP网络设备之间传递控制信息。它主要用于网络连通性测试、错误报告和路径控制;
ICMP还用于查询和诊断网络状态,最常用的工具是ping和traceroute
ping:利用ICMP回送请求(Echo Request)和回送应答(Echo Reply)报文来测试两台主机之间的连通性。
traceroute:通过发送TTL递增的IP数据包并监听ICMP超时报文来探测从源到目的的路径。
3.3 IGMP
用于管理IP多播组成员的协议。它运行在主机和与主机直接相连的多播路由器之间;主机通过IGMP协议通知本地多播路由器,希望加入或离开某个多播组;多播路由器使用IGMP来查询本地网络上的主机是否还是某个多播组的成员
四、传输层
4.1 简介
TCP/UDP都是是传输层协议,但是两者具有不同的特性,同时也具有不同的应用场景;
特性 | TCP (传输控制协议) | UDP (用户数据报协议) |
---|---|---|
连接性 | 面向连接的 数据传输前必须经过三次握手建立连接。 | 无连接的 无需建立连接,即可直接发送数据。 |
可靠性 | 可靠的 通过确认应答、超时重传、序列号等机制确保数据不丢失、不重复、不乱序。 | 不可靠的 不提供任何可靠性机制。数据包可能丢失、重复或乱序。 |
数据传输模式 | 字节流 将数据视为无结构的字节流,不保留消息边界。应用程序自己处理消息边界。 | 数据报 保留消息边界。发送方每次写入一个消息,接收方就会收到一个完整的消息。 |
速度与开销 | 速度较慢,开销大 由于需要建立连接、确认、重传、流量控制等,头部更大(通常20字节),延迟更高。 | 速度极快,开销小 没有连接和可靠性保证的开销,头部极小(仅8字节),延迟低。 |
流量控制与拥塞控制 | 有 使用滑动窗口进行流量控制,并使用复杂的算法(如慢启动、拥塞避免)进行拥塞控制,公平分享网络带宽。 | 无 没有内置的流量或拥塞控制。发送方可以以任何速率发送数据,可能淹没接收方或导致网络拥塞。 |
数据顺序 | 保证顺序 使用序列号确保接收到的数据是按序的。 | 不保证顺序 数据报可能以任何顺序到达。 |
头部大小 | 较大(最小20字节) | 很小(固定8字节) |
双工性 | 全双工 连接建立后,双方可同时发送和接收数据。 | 全双工 双方也可以同时发送和接收数据报。 |
传输单位 | 段 | 数据报 |
适用场景 | 要求数据绝对准确、对延迟不敏感的应用。 例如: • 网页浏览 • 文件传输 • 电子邮件 • 数据库操作 | 要求高速、低延迟,能容忍少量数据丢失的应用。 例如: • 视频流媒体、语音通话 • 在线游戏 • DNS查询 • SNMP网络管理 |
4.2 TCP 报文
源端口号 | 发送方应用程序的端口号 | IP地址将数据包送到正确的主机,而端口号则将数据交给主机上正确的应用程序进程(如Web服务器、SSH服务等) 例如,目的端口 80 代表将数据交给HTTP Web服务。 | |
目的端口号 | 接收方应用程序的端口号 | ||
序列号 | 指本报文段所携带的第一个数据字节的序列号 | 在建立连接时,双方会同步一个初始序列号之后每发送一个字节,序列号就加1 | |
确认号 | 只有在ACK标志位为1时,此字段才有效; 表示期望收到对方下一个报文段的第一个数据字节的序列号。 确认号 = N 的含义是:”所有直到 N-1 的字节我都已经成功收到,请下次从第 N 个字节开始发“ | ||
首部长度 (数据偏移) | 指示TCP报文段的头部长度,即数据部分从哪里开始 | 首部长度占4位且单位为4字节,最大可表示的十进制为15,也就是说TCP报文最大为15*4=60字节,而TCP有20字节的固定首部,还有可选字段(如果有) 即TCP报文的有效负载=60-20-可选字段字节 | |
保留字段 | 为将来使用而保留,必须设置为0 | ||
标志位 | URG(紧急) | URG=1 时,表示报文段中有紧急数据,应尽快传送 此时紧急指针字段有效,指示紧急数据在报文段中的结束位置 | |
ACK(确认) | ACK=1 时,表示确认号字段有效 一旦TCP连接建立成功,所有报文段的ACK位都必须置为1 | ||
PSH(推送) | PSH=1 时,要求接收方立即将数据推送给上层应用程序,而不必等待缓冲区满 例如,在交互式应用(如Telnet)中,每次击键都设置PSH,以便立即回显 | ||
RST(复位) | RST=1 时,表示强制断开连接。通常用于异常情况,如收到无效的连接请求或发生错误 | ||
SYN(同步) | SYN=1 时,表示这是一个连接请求或连接接受报文 用于三次握手过程,来同步序列号 | ||
FIN(终止) | FIN=1 时,表示发送方数据已发送完毕,要求释放连接 用于四次挥手过程 | ||
窗口大小 | 流量控制 | 指示了从确认号开始,接收方还能接收多少字节的数据 这是一个动态变化的字段,接收方通过它来告诉发送方:”我的接收缓冲区还剩这么多空间,你发过来的数据量不要超过这个窗口大小“从而防止快速的发送方淹没慢速的接收方 | |
校验和 | 差错检测 | 发送方计算头部和数据的校验和,接收方重新计算并进行比对 如果校验和不匹配,接收方会直接丢弃该报文段。TCP本身不负责重传,重传由发送方超时未收到确认时触发。 | |
紧急指针 | 与URG标志位配合使用,指示紧急数据的末尾 | 只有在 URG=1 时才有效 其值是当前序列号到紧急数据最后一个字节的偏移量 | |
选项和填充 (长度可变) | 提供一些额外的可选功能 | 常见选项: 最大报文段长度:在三次握手时通信双方协商每个TCP报文段能携带的最大数据量 时间戳:用于更精确地计算往返时间,以及防止序列号回绕 窗口扩大因子:由于原窗口字段只有16位,最大窗口为65535字节。此选项允许将窗口值左移若干位,从而支持更大的窗口(用于高速网络) |
4.3 TCP 数据传输
4.3.1 三次握手
TCP是面向连接的,无论哪一方向另一方发送数据之前,都必须先在双方之间建立一条连接。在TCP/IP协议中,TCP协议提供可靠的连接服务,连接是通过三次握手进行初始化的。三次握手的目的是同步连接双方的序列号和确认号并交换 TCP窗口大小信息
规则:任何消耗序列号的报文(即携带数据或SYN/FIN标志的报文),都会使下一次发送的序列号增加。纯ACK报文(不携带数据,且没有SYN/FIN)不消耗序列号
第一次握手,客户端发送SYN=1,一个随机的序列号seq=x
第二次握手,服务器发送ACK=1,SYN=1,一个随机的序列号seq=y,一个确认号ack=x+1
第三次握手,客户端发送ACK=1,序列号x+1,一个确认号ack=y+1
客户端:最后发送的报文是第三次握手的ACK,其
seq = x+1
。这是一个纯ACK报文(不携带数据,且SYN=0),所以它不消耗序列号。
服务器:最后发送的报文是第二次握手的SYN-ACK,其seq = y
。这是一个SYN报文,消耗一个序列号,所以服务器下一个数据字节的起始序列号应该是y+1
。
因此,连接建立后,双方序列号的起始点是:
客户端:seq = x+1
(因为第三次握手的ACK不消耗序列号)
服务器:seq = y+1
(因为第二次握手的SYN消耗了一个序列号)
4.3.2 数据发送
假设连接建立后,客户端要发送一个数据段,内容为 "Hello"(5个字节)。
第一步:客户端发送数据 "Hello"
客户端 → 服务器
① 数据长度 = 5 字节。
②序列号seq = x+1
(这是客户端要发送的第一个字节的编号)。
③因为客户端还没有从服务器收到任何数据,所以它的确认号ack
依然保持为y+1
(即期望服务器从序列号y+1
开始发送数据)。
④报文内容:[ACK=1, seq=x+1, ack=y+1, data="Hello"]
发送后,客户端序列号更新:x+1 + 5 = x+6
。客户端下次发送数据,序列号将从x+6
开始。
第二步:服务器确认收到 "Hello",并发送自己的数据 "World"
服务器 → 客户端
①服务器成功收到5字节数据。它计算确认号:ack = (x+1) + 5 = x+6
。这个确认号的意思是:“我已完好收到你序列号从x+1
到x+5
的数据,期望你下次从x+6
开始发”。
②服务器也想发送数据 "World"(5字节)。
③它的起始序列号是y+1
。
④报文内容:[ACK=1, seq=y+1, ack=x+6, data="World"]
(这是一个捎带确认,高效地将ACK和数据合并在一个报文中)
发送后,服务器序列号更新:y+1 + 5 = y+6
。
第三步:客户端确认收到 "World"
客户端 → 服务器
①客户端收到服务器的5字节数据。它计算确认号:ack = (y+1) + 5 = y+6
。
②此时客户端可能没有数据要发送,因此它发送一个纯ACK报文。
③报文内容:[ACK=1, seq=x+6, ack=y+6]
(注意,seq
依然是x+6
,因为上一步发送"Hello"后序列号更新为此值,且本次ACK不携带数据,所以序列号不变)
因为这个ACK不携带数据,所以客户端的序列号保持不变,仍为x+6
。
4.3.3 四次挥手
第一次挥手:客户端发起关闭
客户端 -> 服务器
客户端应用程序调用close()
,TCP协议栈会构造一个FIN报文。
标志位:FIN=1, ACK=1
(因为连接已建立,ACK必须为1)。
序列号(seq):x+6
。这是客户端当前要发送的下一个序列号。
确认号(ack):y+6
。这表示客户端仍然期望收到服务器从y+6
开始的数据(虽然它即将关闭接收)。
报文:[FIN=1, ACK=1, seq=x+6, ack=y+6]
序列号变化:FIN标志位消耗一个序列号。因此,客户端发送完这个报文后,其序列号更新为x+7
。
客户端状态:进入FIN-WAIT-1
。
第二次挥手:服务器确认客户端的FIN
服务器 -> 客户端
服务器收到FIN后,知道客户端已经没有数据要发送了。
它必须立即发送一个ACK进行确认。
标志位:ACK=1
。
序列号(seq):y+6
。这是服务器当前要发送的下一个序列号。(注意:这个ACK报文不携带数据,所以不消耗序列号)。
确认号(ack):x+6 + 1 = x+7
。因为客户端的FIN序列号是x+6
,且FIN消耗一个序号,所以服务器通过ack=x+7
来确认:“你的直到x+6
的字节(包括FIN)我已收到,期望你下次从x+7
开始发”。(尽管客户端不会再发数据了)。
报文:[ACK=1, seq=y+6, ack=x+7]
序列号变化:这是一个纯ACK,不消耗序列号。服务器的序列号保持不变,仍为y+6
。
服务器状态:进入CLOSE-WAIT
。
客户端状态:收到此ACK后,进入FIN-WAIT-2
。至此,从客户端到服务器的这个方向的连接已关闭。
注意:此时,服务器可能还有未发送完的数据。它可以在第二次挥手后、第三次挥手前,继续将剩余数据发送给客户端。本例中我们假设数据已发送完毕。
第三次挥手:服务器发起关闭
服务器 -> 客户端
服务器应用程序也调用
close()
,准备关闭连接。
服务器发送自己的FIN报文。
标志位:FIN=1, ACK=1
。
序列号(seq):y+6
。(因为第二步发送的是纯ACK,序列号没变)。
确认号(ack):x+7
。(保持不变,因为客户端自第一次挥手后没有再发送任何数据)。
报文:[FIN=1, ACK=1, seq=y+6, ack=x+7]
序列号变化:FIN标志位消耗一个序列号。服务器发送完这个报文后,其序列号更新为y+7
服务器状态:进入LAST-ACK
。
第四次挥手:客户端确认服务器的FIN
客户端 -> 服务器
客户端收到服务器的FIN后,需要发送最终的ACK进行确认。
标志位:ACK=1
。
序列号(seq):x+7
。(因为第一次挥手的FIN消耗了序列号,且客户端之后没发过数据)。
确认号(ack):y+6 + 1 = y+7
。因为服务器的FIN序列号是y+6
,所以客户端通过ack=y+7
来确认:“你的直到y+6
的字节(包括FIN)我已收到”。
报文:[ACK=1, seq=x+7, ack=y+7
序列号变化:这是一个纯ACK,不消耗序列号。客户端的序列号保持不变。
客户端状态:发送ACK后进入TIME-WAIT
状态,等待2MSL后关闭。
服务器状态:收到这个ACK后,立即关闭连接
4.4 TCP的11种状态
状态名称 | 核心含义 | 触发场景(客户端 / 服务器) | 关键作用与说明 |
---|---|---|---|
CLOSED | 初始/最终状态 | 双方 | 表示没有任何连接状态。一个连接可以是从未建立,也可以是已完全关闭。 |
LISTEN | 等待连接请求 | 服务器(典型) | 服务器调用 listen() 后进入此状态,等待客户端的SYN连接请求。这是服务器的起始状态。 |
SYN-SENT | 已发出连接请求 | 客户端(典型) | 客户端调用 connect() 发送SYN包后进入此状态,等待服务器的SYN-ACK响应。 |
SYN-RCVD | 已收到连接请求 | 服务器 | 服务器收到客户端的SYN并回复SYN-ACK后进入此状态,等待客户端的最终ACK确认。 |
ESTABLISHED | 连接已建立 | 双方 | 三次握手完成,连接成功建立。双方可以开始全双工的数据传输。这是连接的生命期主体。 |
FIN-WAIT-1 | 主动关闭,等待确认 | 主动关闭方(通常为客户端) | 应用程序发起关闭,发送FIN包后进入。等待对方对FIN的ACK,或同时等待对方的FIN。 |
FIN-WAIT-2 | 主动关闭,等待对方关闭 | 主动关闭方 | 已收到对FIN的ACK,形成了半关闭。此时只能接收数据,不能发送,等待对方发送FIN包。 |
CLOSE-WAIT | 被动关闭,等待应用关闭 | 被动关闭方(通常为服务器) | 收到对方的FIN并发送ACK后进入,表示对方已无数据发送。等待本地应用程序调用 close() 。 |
LAST-ACK | 被动关闭,等待最终确认 | 被动关闭方 | 本地应用程序调用 close() ,发送自己的FIN包后进入。等待对方对己方FIN的最终ACK。 |
TIME-WAIT | 等待以确保连接彻底关闭 | 主动关闭方 | 发送完对对方FIN的最终ACK后进入。此状态持续2MSL(通常60秒)。1. 确保最终ACK可达(可重传)。2. 让本次连接的旧报文在网络中消逝,避免与新连接混淆。 |
CLOSING | 双方同时尝试关闭 | 双方(较少见) | 双方几乎同时发送FIN包,并都进入了等待ACK的状态。收到对方的ACK后,会转入TIME-WAIT状态。 |
五、TCP效率策略
5.1 流量控制
流量控制的概念
流量控制是为了防止发送方发送数据过快,超出接收方处理能力的一种机制。TCP协议通过接收和发送缓冲区来实现流量控制。发送方和接收方都维护缓冲区,在数据传输过程中,接收方会通过发送ACK报文告知发送方它的接收缓冲区还剩多少空间。通过这种方式,发送方能够调整数据发送的速率,确保不会导致接收方的缓冲区溢出。
流量控制的实现方式
在TCP连接中,流量控制是通过16位窗口大小字段来实现的。这个字段告诉发送方接收方当前的接收缓冲区大小,也就是接收方的剩余接收空间。发送方根据这个信息调整其发送数据的速率,确保接收方能够处理数据。
连接建立时如何保证数据发送量合理
在TCP连接建立过程中,双方通过三次握手来协商彼此的接收能力。在第一次握手时,发送方会发送一个SYN报文,其中包含了自己的初始序列号(ISN)和最大报文段大小(MSS)。接收方会在第二次握手时回复一个SYN+ACK报文,其中包含接收方的ISN、MSS和窗口大小(即接收缓冲区的剩余空间)。这样,双方就能够知道彼此的接收能力,合理地控制数据发送量。
5.2 滑动窗口
滑动窗口的由来
在没有滑动窗口协议之前,发送方和接收方发送一个数据包后就必须等待接收方的确认。这样,每发送一个包就要等待确认,吞吐量非常低。为了解决这个问题,滑动窗口机制被引入。
滑动窗口允许发送方连续发送多个数据包,并且不必等到前一个数据包的确认就能继续发送下一个包,从而提高了吞吐量和传输效率。滑动窗口的工作原理
在滑动窗口协议中,发送方和接收方分别维护一个发送窗口和接收窗口。窗口内的数据表示可以继续传输的数据范围。接收方通过ACK确认已经收到的数据,并通过窗口大小告知发送方可接收的数据量。
随着发送数据包的到达,发送窗口会向前滑动,表示已发送的数据不再需要重传。接收窗口也会随着ACK确认的到来而滑动,表示接收方已经成功接收的数据。
5.3 拥塞控制
拥塞控制的前提理解
除了流量控制,TCP还需要考虑中间网络的情况。网络可能由于拥堵导致丢包,这时TCP的拥塞控制机制就会启动,避免网络负载过大。拥塞控制的目标是调整发送速度,减少网络压力,确保数据能高效且可靠地传输。
拥塞窗口
拥塞窗口(cwnd)表示当前网络的拥塞情况,它是发送方用于控制发送速率的一个变量。拥塞窗口的大小随着网络状况的变化动态调整,当网络出现拥塞时,拥塞窗口会减小,避免发送过多数据给网络造成更大的压力。
拥塞窗口与发送窗口的关系:
拥塞窗口(cwnd):表示当前网络的拥塞情况,控制发送方可以发送的数据量。
发送窗口(swnd):由接收方的接收窗口(rwnd)和拥塞窗口(cwnd)决定,表示发送方实际可以发送的数据范围。
拥塞控制机制:根据网络状态,发送方会动态调整拥塞窗口的大小,以优化传输效率。
六、TCP API函数
6.1 TCP通信框架
6.2 socket 函数
头文件:#include<sys/types.h> #include<sys/socket.h>
函数原型:int socket(int domain,int type,int protocol);
函数功能:创建一个socket通信,返回通信接口(文件描述符)
函数参数:
@param1:domain 指定使用何种的地址类型
若填AF_INET ,表示IPv4协议;
若填AF_INET6,表示IPv6协议
@param2:type 用于指定传输层协议
若填SOCK_STREAM,表示TCP协议;
若填SOCK_DGRAM,表示UDP协议。
@param3:protocol 协议编号,当type
为SOCK_STREAM
且只想用默认 TCP 时,必须填 0
返回值:
若成功,返回创建的通信接口;网络套接字id
若失败,返回-1
备注:客户端/服务器执行的第一步TCP对应动作:
socket() 仅完成“资源分配 + 初始化”,不产生任何 TCP 报文,也不参与三次握手;
真正的状态迁移和报文收发要从connect
、listen
或accept
开始
6.3 bind 函数
头文件: #include<sys/types.h> #include<sys/socket.h>
函数原型:int bind(int sockfd,struct sockaddr * my_addr,int addrlen);
函数功能:(服务器)为套接字绑定本地信息
函数参数:
@param1:sockfd 套接字(文件描述符),通常填socket()返回值
@param2:sockaddr * my_addr 指向用于存储套接字本地信息的结构体
@param3:addrlen 本地信息的数据长度
返回值:若成功,返回0;若失败,返回-1
备注:1.查看 const struct sockaddr 类型
struct sockaddr
{
unsigned short sa_family; /* 地址族, AF_xxx */
char sa_data[14]; /* 14 字节的协议地址,包括Socket的IP和端口 */
};
成员说明:
sa_family :是2字节的地址家族,它的值包括三种:
AF_INET : IPV4
AF_INET6 : IPV6 :
AF_UNSPEC : IPV4/IPV6 都可以,不能出现在
bind()
的sin_family
字段2.分析发现 IP 和端口 不好填充(IP和端口总共6字节 而 sa_data 有14个字节空间,怎么放),所以在通信是不是接着对 struct sockaddr 填充,而是对 struct sockaddr_in
通用sockaddr
只是外壳,实际填充sockaddr_in
再强转3.查看 struct sockaddr_in 类型 <netinet/in.h>
struct sockaddr_in
{
short int sin_family; /* Address family IP类型*/
unsigned short sin_port; /* Port number 端口号*/
struct in_addr sin_addr; /* Internet address IP地址*/
unsigned char sin_zero[8]; /* Same size as struct sockaddr */
};
sin_family:指代协议族,在socket编程中只能是AF_INET,表示IPv4协议
sin_port:存储端口号(使用网络字节顺序)
sin_addr:存储IP地址,使用in_addr这个数据结构
sin_zero:是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节
看到IP地址 :
struct in_addr
{
in_addr_t s_addr; // IPv4地址
};
4.数据转换分析
在网络中传输数据时,通常是按照大端模式进行传输,但是计算机中存储数据时,通常是按照小端模式进行处理。因此需要使用一些大小端转换的函数。大端存储:数据的低字节存放在高地址,数据的高字节存放在低地址--网络传输
小端存储:数据的低字节存放在低地址,数据的高字节存放在高地址--MCU本地
网络/主机的大小端转换,用法类似,参数提供原数据,函数返回转换后的数据
htonl() 将long型数据从主机网络转换 #include
<arpa/inet.h>
htons() 将short型数据从主机网络转换
ntohl() 将long型数据从网络主机转换
ntohs() 将short型数据从网络主机转换
将主机IP地址字符串转换网络字节序的32bitIP地址数值
int inet_
pton
(int af,const char *src,void*det);Eg: 将192.168.0.10转换为32bit;
inet_pton(AF_INET, "192.168.0.10", &sockaddr_in.sin_addr)或:unsigned long int inet_addr(const char *cp); Eg: unsigned long addr = inet_addr(“192.168.0.10”);Eg:struct sockaddr_in seraddr;
int len=sizeof(struct sockaddr_in);
bzero(&seraddr,len);//memset(&seraddr, 0, sizeof(seraddr));
seraddr.sin_family = AF_INET;
// 服务器 端口 >1023 即可
seraddr.sin_port = htons(8888);
// 服务器IP 真实有效 --和主机IP 一致
seraddr.sin_addr.s_addr = inet_
pton
("192.168.10.106");ret = bind(serfd, (struct sockaddr*)&seraddr, sizeof(struct sockaddr));
TCP对应动作
内核为套接字钉死了本地 IP 和端口号,为后续 connect 或 listen 提供“源地址”基础;
bind 本身不产生 SYN、RST、FIN 任何报文,也不进入三次握手流程
6.4 listen 函数
头文件:#include<sys/socket.h>
函数原型:int listen(int s,int backlog);
函数功能:创建一条监听队列
函数参数:
@param1:s 套接字(文件描述符),通常填socket()返回值
@param2:backlog 已完成三次握手、等待 accept 的 ESTABLISHED(连接已建立) 队列最大长度
返回值:若成功,返回0;若失败,返回-1;
备注:listen()只适用 SOCK_STREAM 或 SOCK_SEQPACKET 的 socket类型。如果 socket 为 AF_INET 则参数 backlog默认值为 128TCP对应动作
内核从此对该套接字进入 LISTEN 状态,具备接受并发三次握手的能力;
用户层后续 accept() 只是从已完成的握手队列里取出 fd,不再参与握手过程
6.5 accept 函数
头文件:#include<sys/types.h> #include<sys/socket.h>
函数原型:int accept(int s,struct sockaddr * addr,socklen_t * addrlen);
函数功能:从监听套接字的已完成连接队列里取出一个新连接,返回全新的已连接套接字
函数参数:
@param1:s 监听套接字,通常填listen()参数
@param2:addr 用于返回连接成功的那个客户端的地址信息(不需要我们写,通过地址传递返回客户端地址信息)
@param3:addrlen 既是输入也是输出;调用前必须初始化为addr
指向缓冲区的大小(告诉内核别写越界)返回时被内核改为实际写入的地址长度
返回值:
若成功,返回和本次连接的客户端通信的套接字,后续服务器可以通过这个套接字来对本次连接的这一个客户端进行收发数据;
若失败,返回-1.
备注:
服务器进程调用该函数后将阻塞,直到被一个客户端发起连接
每次只取一个已连接套接字,但可以循环调用,服务任意数量客户端TCP 状态机动作:
服务器端:
监听套接字本身仍处于 LISTEN;
内核把已完成三次握手的条目从全连接队列摘下,新建一个已连接套接字,状态变为 ESTABLISHED,accept
返回其描述符。客户端:
收到服务器的 SYN+ACK 后发送 ACK,状态由 SYN_SENT → ESTABLISHED。
6.6 send 函数
头文件:#include<sys/types.h> #include<sys/socket.h>
函数原型:ssize_t
send(int s,const void * msg,int len,int flags);
函数功能:通过套接字向对方发送数据”——只对已连接套接字有效;它并不保证本次调用就把 len 字节全部发出去;返回值 ≤ len,剩余要由调用者再send
函数参数:
@param1: s 已连接套接字的 fd
@param2: msg 指向待发送数据
@param3:len 期望发送数据的长度
@param4:flags 通常填0
MSG_DONTWAIT
非阻塞;
MSG_MORE
告诉 TCP 还有数据,推迟推段,利于组包;
MSG_NOSIGNAL
避免SIGPIPE
;
MSG_OOB
发送带外数据
返回值:≥0:实际拷贝进内核发送缓冲区的字节数;若失败,返回-1TCP 对应动作:
把用户数据按 MSS 分段(考虑路径 MTU、拥塞窗口、通告窗口);
每段封装 TCP 头、赋予序号,放入发送队列
由 TCP 状态机决定何时发出去(可能立即发、可能攒批、可能因拥塞推迟)
对端 ACK 后内核才释放缓冲区里的对应段
6.7 recv 函数
头文件:#include<sys/types.h> #include<sys/socket.h>
函数原型:ssize_t
recv(int s,void *buf,int len, int flags);
函数功能:从内核接收缓冲区拷贝数据到用户空间;若缓冲区空且套接字为阻塞模式,则阻塞直到有数据或对方关闭
函数参数:
@param1: s 已连接套接字描述符
@param2: buf 指向存储接收数据的空间
@param3:len 期望接收数据的长度
@param4:flags 通常填0
MSG_DONTWAIT
非阻塞
MSG_PEEK
偷看数据不删除
MSG_OOB
读取带外数据
MSG_WAITALL
等待凑满len再返回(对TCP可用)返回值:
>0
实际拷贝字节数;=0
对端已优雅关闭(FIN 已收且缓冲区读空);-1
出错
备注:若对方关闭了通信,则recv()调用后直接返回,且返回值为0TCP 对应动作:
检查接收缓冲区 → 有数据立即拷贝并更新序列号;
缓冲区空且连接仍 ESTABLISHED:阻塞线程(或非阻塞返回
EAGAIN
);若收到对端 FIN 且缓冲区已读空,返回 0 表示 EOF;
之后若再 recv,仍返回 0(不会阻塞)
6.8 connect 函数
头文件:#include<sys/types.h> #include<sys/socket.h>
函数原型:int connect (int sockfd,struct sockaddr * serv_addr,int addrlen);
函数功能:建立socket连线,向服务器发起连接;对 TCP 会阻塞(默认)完成三次握手;对 UDP 仅把对端地址记录到内核,不发任何报文
函数参数:
@param1: sockfd 通信套接字
@param2: serv_addr 指向服务器的地址信息--和 服务器端 bind绑定的服务器信息保持一致
@param3:addrlen 服务器地址信息长度
返回值:若成功,返回0;若失败,返回-1
TCP 对应动作:
随机选本地端口,发送 SYN,状态 → SYN_SENT;
收到服务器 SYN+ACK,回 ACK,状态 → ESTABLISHED;
才返回到用户空间。
6.9 shutdown 函数
头文件:#include<sys/socket.h>
函数原型:int shutdown(int s,int how);
函数功能:用来终止参数 s 所指定的 socket 连线
函数参数:
@param1: s 通信套接字(保存对方信息/远程信息)
@param2: how=0 终止读取操作。how=1 终止传送操作how=2 终止读取及传送操作
返回值:成功则返回 0,失败返回-1
6.9 close 函数
头文件:#include <unistd.h>
函数原型:int close(int fd);
函数功能:销毁进程内指定的文件描述符(fd),使其不再指向任何内核打开文件/socket/管道;当对应内核对象的引用计数减至 0 时,才真正回收资源并触发底层关闭逻辑(如 TCP 发送 RST 或 FIN)。
函数参数 fd:要关闭的文件描述符,由 open/socket/pipe/dup 等返回。
返回值:成功:0,失败:-1,并置 errno 以指示错误(常见 EBADF、EINTR)
备注:
仅减少引用计数;若此前 dup/fork 过,实际连接仍保持。
对 socket 若未先 shutdown,引用计数归 0 时将自动发送 RST(非优雅关闭)。
优雅关闭应先 shutdown(fd, SHUT_WR) 发 FIN,再 close。
七、代码
7.1 通过TCP实现客户端发送数据,服务端接收数据 ,输入byebye都结束
/********************************客户端**************************************/
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>int main(int argc,char*argv[])
{int Socket_fd;//套接字IDif(argc!=3){printf("argc error\n");return -1;}/*1.创建套接字,IPv4,TCP*/Socket_fd= socket(AF_INET,SOCK_STREAM,0);if(Socket_fd==-1){printf("socket create error\n");return -2;}/*2.建立socket连线,向服务器发起连接*/struct sockaddr_in Addr_In;// sockaddr类型的结构体Addr_In.sin_family= AF_INET;//IPv4Addr_In.sin_port=htons(atoi(argv[2]));//端口号if(inet_pton(AF_INET,argv[1],&Addr_In.sin_addr.s_addr)<=0){ printf("IP address error\n");close(Socket_fd);return -3;}if(connect(Socket_fd,(struct sockaddr *)&Addr_In,sizeof(Addr_In))==-1){printf("connect create error\n");close(Socket_fd);return -4;}printf("Connect to server %s:%s successfully!\n", argv[1], argv[2]);/*发数据*/char SendBuff[128];//从标准输入接收发送的数据ssize_t send_len;//实际拷贝进内核发送缓冲区的字节数printf("connect successfully! please input (type 'byebye' to quit):\n");while(1){if(fgets(SendBuff, sizeof(SendBuff), stdin)==NULL){printf("Input error or EOF\n");break;}SendBuff[strcspn(SendBuff,"\n")]='\0'; //去除'\n'if(strlen(SendBuff)==0) //检查是否为空{continue; } send_len=send(Socket_fd,SendBuff,strlen(SendBuff),0);//发送数据if(send_len==-1){printf("send error\n");break;}printf("Sent: %s\n", SendBuff);if(strcmp(SendBuff,"byebye")==0){ printf("Quitting...\n");break;}}/*关闭*/printf("Close Client TCP.\n");shutdown(Socket_fd,2);close(Socket_fd);return 0;
}/********************************服务端**************************************/
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
/*./client IP地址 端口号*/
int main(int argc,char*argv[])
{int Socket_fd; //套接字IDif(argc!=3){printf("argc error\n");return -1;}/*1.创建套接字,IPv4,TCP*/Socket_fd= socket(AF_INET,SOCK_STREAM,0);if(Socket_fd==-1){printf("socket create error\n");return -2;}/*2.为服务器绑定本地信息*/struct sockaddr_in Addr_In; // sockaddr类型的结构体memset(&Addr_In, 0, sizeof(Addr_In)); // 清空结构体Addr_In.sin_family= AF_INET; //IPv4Addr_In.sin_port=htons(atoi(argv[2])); //端口号 if(inet_pton(AF_INET,argv[1],&Addr_In.sin_addr.s_addr)<=0) //IP地址转化为32bit放入Addr_In.sin_addr.s_addr中{ printf("IP address error\n");close(Socket_fd);return -3;}int ret= bind(Socket_fd,(struct sockaddr *)&Addr_In ,sizeof(Addr_In));if(ret==-1){printf("bind error\n");close(Socket_fd);return -4;}/*3.创建监听队列*/if(listen(Socket_fd,5)==-1) //5表示最多5个已完成连接可以排队等待accept{printf("listen error\n");close(Socket_fd);return -5;}printf("Server started on %s:%s\n", argv[1], argv[2]);/*4.阻塞等待客户端连接*/struct sockaddr_in ClientAddr; //客户端的地址信息socklen_t AddrLen = sizeof(ClientAddr); //初始化长度为缓冲区大小int ClientSocket_fd= accept(Socket_fd,(struct sockaddr *)&ClientAddr,&AddrLen);if(ClientSocket_fd==-1){printf("accept error\n");close(Socket_fd);return -6;}char client_ip[16];//用于存放字符串形式的IPinet_ntop(AF_INET, &ClientAddr.sin_addr.s_addr, client_ip, sizeof(client_ip));//从Addr_In.sin_addr.s_addr取出二进制IP后转换为字符串形式的IP方入client_ip中printf("Client connected from %s:%d, socket fd: %d\n", client_ip, ntohs(ClientAddr.sin_port), ClientSocket_fd);//将ClientAddr.sin_port中的端口号转换/*5.收数据*/char RecvBuff[128];ssize_t RecvLen;//>0表示实际拷贝的字节数while(1){RecvLen=recv(ClientSocket_fd,RecvBuff,sizeof(RecvBuff)-1,0);// 保留1字节给null终止符if(RecvLen==0){printf("Client has disconnected gracefully.\n");break;}else if(RecvLen==-1)//对端已经关闭{printf("Receive error occurred.\n");break;}RecvBuff[RecvLen]='\0';//在接收的数据后添加'\0'表示字符串结束if(RecvLen>0&&RecvBuff[RecvLen-1]=='\n')//如果最后一个字符为换行符,则将换行符替换为'\0'{RecvBuff[RecvLen-1]='\0';}printf("Server recv:%s\n",RecvBuff);//输出if(strcmp(RecvBuff,"byebye")==0){printf("Client requested to quit.\n");break;}}/*6.关闭*/printf("Close Sever TCP.\n");close(ClientSocket_fd);close(Socket_fd);return 0;
}
7.2 实现一组C + S之间的多次双向对话
要求:
- 服务器能够接收1个客户端的连接
- 连接成功后,双方都可以键盘输入数据,按下回车键实现发送(给对方)
- 当对方发送成功后,需要及时显示接收的内容
- 任意一方发送“Quit\n”则双方停止会话
- 当任意一方离线,则对方需要提示“Server/Client 已离线”
注意:当在服务器端关闭程序后就立即相同的端口运行服务器端口,会出现端口占用的现象
/***************【服务器】**********************/
//./serve IP 端口号#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include <pthread.h>
#include<unistd.h>
#include<netinet/in.h>int SocketClientID ; //用于接收新的已连接套接字ID
pthread_t RecvId,SendId ; //读写线程ID
void*pthreadRecv_function(void*arg)
{char RecvBuff[128] ;ssize_t ret ; //实际接收的字节数while(1){ret=recv(SocketClientID,RecvBuff,sizeof(RecvBuff)-1,0); if(ret==0){printf("client has close\n") ;goto close_function ;}else if(ret==-1){printf("pthreadRecv_function error\n") ;goto close_function ;}RecvBuff[ret]='\0' ;//除去换行符if(ret>0&&RecvBuff[ret-1]=='\n'){RecvBuff[ret-1]='\0' ;}printf("Serve Recv:%s\n",RecvBuff) ;if(strcmp(RecvBuff,"Quit")==0){goto close_function ;}}
close_function:pthread_cancel(SendId) ;return NULL ;
}
void*pthreadSend_function(void*arg)
{char SendBuff[128] ;ssize_t ret ;//实际发送的字节数while(1){if(fgets(SendBuff,sizeof(SendBuff),stdin)==NULL){printf("fgets error\n") ;goto close_function ;}SendBuff[strcspn(SendBuff,"\n")]='\0' ;//去除'\n'if(strlen(SendBuff)==0) //检查是否为空{continue ;}ret=send(SocketClientID,SendBuff,strlen(SendBuff),0) ;//发数据if(ret==-1){goto close_function ;}printf("serve send:%s\n",SendBuff) ;if(strcmp(SendBuff,"Quit")==0){goto close_function ;}}
close_function:pthread_cancel(RecvId) ;return NULL ;
}int main(int argc,char *argv[])
{ if(argc!=3){printf("argc error\n") ;printf("please input ./serve IP地址 端口号\n") ;return -1 ;}/*1.创建套接字、IPv4,TCP*/ int SocketID ;//用于接收套接字IDSocketID = socket(AF_INET,SOCK_STREAM,0);if(SocketID==-1){printf("socket error\n") ;return -2 ;}/*2.为套接字绑定本地信息*/struct sockaddr_in ServeAddr;memset(&ServeAddr,0,sizeof(ServeAddr)) ;//清空ServeAddrServeAddr.sin_family=AF_INET ;//IP类型ServeAddr.sin_port=htons(atoi(argv[2])) ;//将 argv[2]从字符串变为数字再变为大端模式if(inet_pton(AF_INET,argv[1],&ServeAddr.sin_addr)!=1){printf("inet_pton error\n") ;close(SocketID) ;return -3 ;}if(bind(SocketID,(struct sockaddr *)&ServeAddr,sizeof(ServeAddr))==-1){printf("bind error\n") ;close(SocketID) ;return -4 ;}/*3.创建监听队列,设定可以监听10个TCP客户端*/if(listen(SocketID,10)==-1){printf("listen error\n") ;close(SocketID) ;return -5 ;}printf("accepting……,please wait\n");/*4.从监听套接字的已完成连接队列里取出一个新连接,返回全新的已连接套接字*/struct sockaddr_in ClientAddr ;//用于接收成功握手的客户端信息memset(&ClientAddr,0,sizeof(ClientAddr)) ;//清空ClientIDsocklen_t ClientAddr_Len=sizeof(ClientAddr) ;SocketClientID= accept(SocketID,(struct sockaddr *)&ClientAddr,&ClientAddr_Len);if(SocketClientID==-1){printf("accept error\n") ;close(SocketID) ;return -6 ;}char ClientIp[16]={0} ;//用于接收对方IPprintf("one client has connect,the ip:%s,the port:%d\n",inet_ntop(AF_INET,ClientAddr.sin_addr,ClientIp,sizeof(ClientIp)),ntohs(ClientAddr.sin_port));/*5.消息读写,这里创建两个线程*/if(pthread_create(&RecvId,NULL,pthreadRecv_function, NULL)!=0){printf("pthread_create Recv error\n") ;close(SocketClientID) ;close(SocketID) ;return -7 ;}if(pthread_create(&SendId,NULL,pthreadSend_function, NULL)!=0){printf("pthread_create Send error\n") ;close(SocketClientID) ;close(SocketID) ;return -8 ;}pthread_join(RecvId, NULL) ;pthread_join(SendId, NULL) ;/*关闭套接字*/printf("Closing server...\n") ;close(SocketClientID) ;close(SocketID) ;return 0 ;
}/*******************【客户端】******************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include <pthread.h>
#include<unistd.h>
#include<netinet/in.h>int SocketID ;//用于接收套接字ID
pthread_t RecvId,SendId ;//读写线程IDvoid*pthreadRecv_function(void*arg)
{char RecvBuff[128] ;ssize_t ret ;//实际接收的字节数while(1){ret=recv(SocketID,RecvBuff,sizeof(RecvBuff)-1,0) ; if(ret==0){printf("Serve has close\n") ;goto close_function ;}else if(ret==-1){printf("pthreadRecv_function error\n") ;goto close_function ;}RecvBuff[ret]='\0' ;//除去换行符if(ret>0&&RecvBuff[ret-1]=='\n'){RecvBuff[ret-1]='\0' ;}printf("Client Recv:%s\n",RecvBuff) ;if(strcmp(RecvBuff,"Quit")==0){goto close_function ;}}
close_function:pthread_cancel(SendId) ;return NULL ;
}
void*pthreadSend_function(void*arg)
{char SendBuff[128] ;ssize_t ret ;//实际发送的字节数while(1){if(fgets(SendBuff,sizeof(SendBuff),stdin)==NULL){printf("fgets error\n") ;goto close_function ;}SendBuff[strcspn(SendBuff,"\n")]='\0' ;//去除'\n'if(strlen(SendBuff)==0) //检查是否为空{continue ;}ret=send(SocketID,SendBuff,strlen(SendBuff),0);//发数据if(ret==-1){goto close_function ;}printf("Client send:%s\n",SendBuff) ;if(strcmp(SendBuff,"Quit")==0){goto close_function ;}}
close_function:pthread_cancel(RecvId) ;return NULL ;
}
int main(int argc,char *argv[])
{if(argc!=3){printf("argc error\n") ;printf("please input ./client IP地址 端口号\n");return -1 ;}/*1.创建套接字、IPv4,TCP*/ SocketID = socket(AF_INET,SOCK_STREAM,0) ;if(SocketID==-1){printf("socket error\n") ;return -2 ;}/*2.连接服务器*/struct sockaddr_in ServeAddr ;memset(&ServeAddr,0,sizeof(ServeAddr)) ;//清空ServeAddrServeAddr.sin_family=AF_INET ;//IP类型ServeAddr.sin_port=htons(atoi(argv[2])) ;//将 argv[2]从字符串变为数字再变为大端模式if(inet_pton(AF_INET,argv[1],&ServeAddr.sin_addr)!=1)//IP地址转换32bit{printf("inet_pton error\n") ;close(SocketID) ;return -3 ;}if(connect(SocketID,(struct sockaddr *)&ServeAddr,sizeof(ServeAddr))==-1){printf("connect error\n") ;close(SocketID) ;return -4 ;}printf("Connect to server %s:%s successfully!\n", argv[1], argv[2]);/*3.读写线程*/if(pthread_create(&RecvId,NULL,pthreadRecv_function, NULL)!=0){printf("pthread_create Recv error\n") ;close(SocketID) ;return -5 ;}if(pthread_create(&SendId,NULL,pthreadSend_function, NULL)!=0){printf("pthread_create Send error\n") ;close(SocketID) ;return -6 ;}pthread_join(RecvId, NULL);pthread_join(SendId, NULL); /*关闭套接字*/printf("Closing client...\n");close(SocketID) ;return 0 ;
}
3.实现C可从S下载文件
思路:服务端有源文件,服务端将文件目录发送给客户端,客户端根据文件目录的序号选择要下载的文件,然后将序号发送给服务端,服务端根据客户端选择的序号找到对应的文件并形成文件路径,打开文件,就源文件内容传输;客户端根据序号在文件目录中找到对应文件名,并结合存放的目录形成新的文件路径,接收来着服务端的源文件内容。
/***************【服务器】**********************/
/*要求:
1. 服务器能够接收1个客户端的连接
2. 服务器相关路径中存在一个文件夹“files”,这个文件夹中将提供一些文件(预先准备好,比如放一些文本文件、图片文件、音乐文件等)
客户端相关路径中存在一个文件夹“download”,这个文件夹将用于存储之后下载的文件
3. 当C和S连接成功后,首先服务器给客户端发送一个字符串,字符串内容包含可以下载的文件。
you can download these files:
1、 123.txt
2、 dog.bmp
3、 只因你太美.mp3
if you want to download, please input number of file:
*///./serve IP 端口号#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include <pthread.h>
#include<unistd.h>
#include<netinet/in.h>
#include<sys/types.h>
#include<dirent.h>
#include<fcntl.h>
int main(int argc,char*argv[])
{if(argc!=3){printf("argc error,please input ./server IP port");return -1 ;}/*1.创建套接字,IPv4,TCP*/int SocketID=socket(AF_INET,SOCK_STREAM,0);if(SocketID==-1){printf("socket error\n") ;return -2 ;}/*2.绑定本地信息*/struct sockaddr_in LocalMsg ;//本地信息memset(&LocalMsg,0,sizeof(LocalMsg)) ;LocalMsg.sin_family=AF_INET ;//IPv4LocalMsg.sin_port=htons(atoi(argv[2])) ;if(inet_pton(AF_INET,argv[1], &LocalMsg.sin_addr)!=1){printf("inet_pton error\n");close(SocketID) ;return -3 ;}if(bind(SocketID,(struct sockaddr*)&LocalMsg,sizeof(LocalMsg))==-1){printf("bind error\n") ;close(SocketID) ;return -4 ;}/*3.监听队列*/if(listen(SocketID,5)==-1){printf("listen error\n") ;close(SocketID) ;return -5 ;}printf("Server started on %s:%s\n", argv[1], argv[2]);printf("Waiting for client connection...\n") ;/*4.接收客户端信息*/struct sockaddr_in ClientMsg ;//客户信息memset(&ClientMsg,0,sizeof(ClientMsg)) ;socklen_t ClientMsgLen=sizeof(ClientMsg) ;int SocketClientID = accept(SocketID,(struct sockaddr * )&ClientMsg,&ClientMsgLen);if(SocketClientID==-1){printf("accept error\n") ;close(SocketID) ;return -6 ;}char IP[16]={0} ;inet_ntop(AF_INET,&ClientMsg.sin_addr,IP,sizeof(IP)) ;printf("One client has connected!,the IP:%s,Port:%d\n",IP,ntohs(ClientMsg.sin_port));/*5.将文件中的目录读取到buff中*///打开文件目录DIR *FileID ;FileID=opendir("./file") ;if(FileID==NULL){printf("opendir error\n");close(SocketClientID) ;close(SocketID) ;return -7 ;}//读文件struct dirent * FileMsg=NULL ;int cnt =0 ;char FileName[64][64] ;//存放文件中的文件名char SendBuff[1024] ;//发送缓存区char temp[128];strcpy(SendBuff,"there are the file contains:\n") ;while((FileMsg = readdir(FileID))!=NULL){if(strcmp(FileMsg->d_name,".")==0||strcmp(FileMsg->d_name,"..")==0){continue;}cnt ++ ;strcpy(FileName[cnt],FileMsg->d_name) ;//保存文件名sprintf(temp,"%d.%s\n",cnt,FileMsg->d_name);//拼接文件列表符串strcat(SendBuff,temp) ;//拼接}closedir(FileID) ;//关闭文件strcat(SendBuff,"if you want to download, please input the number of file\n");// 添加提示信息/*6.发送到客户端*/if(send(SocketClientID,SendBuff,strlen(SendBuff),0)==-1){printf("send error\n") ;close(SocketClientID) ;close(SocketID) ;return -8 ;}printf("FileName[1]=%s\nFileName[2]=%s\nFileName[3]=%s\n",FileName[1],FileName[2],FileName[3]);/*7.从客户端接收*/char RecvBuff[1024]={0} ;ssize_t ret = recv(SocketClientID,RecvBuff,sizeof(RecvBuff)-1, 0);printf("RecvBuff:%s,ret=%d\n",RecvBuff,ret);if(ret==0){printf("client has close\n");close(SocketClientID) ;close(SocketID) ;return -9 ;}else if(ret ==-1){printf("recv error\n") ;close(SocketClientID) ;close(SocketID) ;return -10 ; }RecvBuff[ret]='\0' ;int seq= atoi(RecvBuff) ;//处理接收到的数据printf("seq=%d\n",seq) ;//测试用if(seq<1||seq>cnt){printf("client select error\n") ;char errormsg[]="Invalid file number!\n" ;send(SocketClientID,errormsg,strlen(errormsg),0);close(SocketClientID) ;close(SocketID) ;return -11 ; }printf("the client choose the file:%s\n",FileName[seq]);char DownFileName[128] ;sprintf(DownFileName,"./flie/%s",FileName[seq]) ;//完整的文件名路径//读文件里面的内容int Fd =open(DownFileName,O_RDONLY) ;if(Fd<0){printf("open the file error\n") ;char errormsg[]="open the file error\n";send(SocketClientID,errormsg,strlen(errormsg),0) ;close(SocketClientID) ;close(SocketID) ;return -12 ; }printf("Sending file: %s\n", DownFileName) ;ssize_t ReadLen ;char TempBuff[1024];ssize_t send_len ;while((ReadLen= read(Fd,TempBuff,sizeof(TempBuff)))>0){send_len=send(SocketClientID,TempBuff,ReadLen,0);printf("TempBuff=%s",TempBuff);//测试用if(send_len==-1){printf("Error sending file data\n") ;break ;}}close(Fd) ;/*7.关闭服务器*/ close(SocketClientID) ;close(SocketID) ;printf("Server closed.\n");return 0 ;
}
/*******************【客户端】******************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <unistd.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <sys/stat.h>int main(int argc,char *argv[])
{if(argc!=3){printf("argc error\n") ;printf("please input ./client IP地址 端口号\n");return -1 ;}int SocketID ;//用于接收套接字ID/*1.创建套接字、IPv4,TCP*/ SocketID = socket(AF_INET,SOCK_STREAM,0) ;if(SocketID==-1){printf("socket error\n") ;return -2 ;}/*2.连接服务器*/struct sockaddr_in ServeAddr ;memset(&ServeAddr,0,sizeof(ServeAddr)) ;//清空ServeAddrServeAddr.sin_family=AF_INET ;//IP类型ServeAddr.sin_port=htons(atoi(argv[2])) ;//将 argv[2]从字符串变为数字再变为大端模式if(inet_pton(AF_INET,argv[1],&ServeAddr.sin_addr)!=1)//IP地址转换32bit{printf("inet_pton error\n") ;close(SocketID) ;return -3 ;}if(connect(SocketID,(struct sockaddr *)&ServeAddr,sizeof(ServeAddr))==-1){printf("connect error\n") ;close(SocketID) ;return -4 ;}printf("Connect to server %s:%s successfully!\n", argv[1], argv[2]);/*3.接收服务器发送的消息*/char RecvBuff[1024] ;//文件夹中的所有文件名ssize_t ret = recv(SocketID,RecvBuff,sizeof(RecvBuff)-1,0);if(ret<=0){printf("recv error\n") ;close(SocketID) ;return -5 ;}RecvBuff[ret]='\0' ;if(ret>0&&RecvBuff[ret-1]=='\n'){RecvBuff[ret-1]='\0' ;}// 显示文件列表printf("%s\n", RecvBuff);/*4.选择*/char buf[32] ;//用于接收文件号printf("Please input file number: ") ;fgets(buf,sizeof(buf),stdin) ;buf[strcspn(buf, "\n")] = '\0' ;//移除换行符int seq = atoi(buf) ;//将序列号从字符转为数字if(send(SocketID,buf,strlen(buf),0)==-1){printf("send error\n") ;close(SocketID) ;return -6 ;}/*5.获取文件夹名*/char TempNameBuff[64] ;sprintf(TempNameBuff,"%d.",seq) ;//文件序号printf("TempNameBuff=%s\n",TempNameBuff);//测试用char*p=strstr(RecvBuff,TempNameBuff) ;//返回RecvBuff出现文件序号的首地址;if(p==NULL){printf("p error\n") ;close(SocketID) ;return -7 ; }p+=strlen(TempNameBuff) ;//p指到文件名起始处char*q=strstr(p,"\n") ;//q指到文件名结束处char FileName[64] ;//用于存放要下载的文件名strncpy(FileName,p,q-p) ;FileName[q-p]='\0' ;//要下载的文件名printf("choose the file :%s\n",FileName);//新建文件路径sprintf(TempNameBuff,"./DownLoad/%s",FileName) ;printf("TempNameBuff=%s\n",TempNameBuff) ;//测试用//打开文件int Fd = open(TempNameBuff,O_RDWR|O_CREAT|O_TRUNC,0666);//open 只能创建文件,不会自动帮你创建目录ssize_t RecvLen ;//接收的长度while((RecvLen=recv(SocketID,TempNameBuff,sizeof(TempNameBuff),0))>0){printf("已经开始写\n") ;//测试用write(Fd,TempNameBuff,RecvLen) ;printf("TempNameBuff=%s\n",TempNameBuff); //测试用 }close(Fd) ;/*关闭套接字*/printf("Closing client...\n");close(SocketID) ;return 0 ;
}