【一】CS架构与BS架构
- C/S和B/S都是互联网中常见的网络结构模型。
【1】什么是C/S模型
- C是英文单词“Client”的首字母,即客户端的意思
- C/S就是“Client/Server”的缩写,即“客户端/服务器”模式。
- 例如:拼多多APP、PC上的有道云笔记等等
【2】什么是B/S模型
- B是英文单词“Browser”的首字母,即浏览器的意思;
- S是英文单词“Server”的首字母,即服务器的意思。
- B/S就是“Browser/Server”的缩写
- 即“浏览器/服务器”模式。
- 例如:淘宝网、京东网等等
- 即“浏览器/服务器”模式。
【3】两个模型比较
- C/S和B/S是当今世界开发模式技术架构的两大主流技术。
- C/S是美国Borland公司最早研发
- B/S是美国微软公司研发。
- 目前,这两项技术以被世界各国所掌握
- 国内公司以C/S和B/S技术开发出产品也很多。
- 这两种技术都有自己一定的市场份额和客户群,各家企业都说自己的管理软件架构技术功能强大、先进、方便,都能举出各自的客户群体
【4】C/S架构软件的优势与劣势
- 优势
- 针对客户端可以高度定制
- 劣势
- 需要用户去下载才能使用,不方便
【1】应用服务器运行数据负荷较轻
- 最简单的C/S体系结构的数据库应用由两部分组成,即客户应用程序和数据库服务器程序。
- 二者可分别称为前台程序与后台程序。
- 运行数据库服务器程序的机器,也称为应用服务器。
- 一旦服务器程序被启动,就随时等待响应客户程序发来的请求;
【2】数据的储存管理功能较为透明
- 在数据库应用中,数据的储存管理功能,是由服务器程序和客户应用程序分别独立进行的,前台应用可以违反的规则,并且通常把那些不同的(不管是已知还是未知的)运行数据,在服务器程序中不集中实现
- 例如访问者的权限,编号可以重复、必须有客户才能
- 建立定单这样的规则。
- 所有这些,对于工作在前台程序上的最终用户,是“透明”的,他们无须过问(通常也无法干涉)背后的过程,就可以完成自己的一切工作。
- 在客户服务器架构的应用中,前台程序不是非常“瘦小”,麻烦的事情都交给了服务器和网络。
- 在C/S体系的下,数据库不能真正成为公共、专业化的仓库,它受到独立的专门管理。
【3】C/S架构的劣势是高昂的维护成本且投资大
- 首先,采用C/S架构,要选择适当的数据库平台来实现数据库数据的真正“统一”,使分布于两地的数据同步完全交由数据库系统去管理,但逻辑上两地的操作者要直接访问同一个数据库才能有效实现,有这样一些问题
- 如果需要建立“实时”的数据同步,就必须在两地间建立实时的通讯连接,保持两地的数据库服务器在线运行,网络管理工作人员既要对服务器维护管理,又要对客户端维护和管理,这需要高昂的投资和复杂的技术支持,维护成本很高,维护任务量大。
- 其次,传统的C/S结构的软件需要针对不同的操作系统系统开发不同版本的软件,由于产品的更新换代十分快,代价高和低效率已经不适应工作需要。
- 在JAVA这样的跨平台语言出现之后,B/S架构更是猛烈冲击C/S,并对其形成威胁和挑战。
【5】B/S架构软件的优势与劣势
【1】维护和升级方式简单
- 目前,软件系统的改进和升级越来越频繁,B/S架构的产品明显体现着更为方便的特性。
- 对一个稍微大一点单位来说,系统管理人员如果需要在几百甚至上千部电脑之间来回奔跑,效率和工作量是可想而知的
- 但B/S架构的软件只需要管理服务器就行了
- 所有的客户端只是浏览器,根本不需要做任何的维护。无论用户的规模有多大,有多少分支机构都不会增加任何维护升级的工作量
- 所有的操作只需要针对服务器进行;
- 如果是异地,只需要把服务器连接专网即可,实现远程维护、升级和共享。
- 所以客户机越来越“瘦”,而服务器越来越“胖”是将来信息化发展的主流方向。
- 今后,软件升级和维护会越来越容易,而使用起来会越来越简单,这对用户人力、物力、时间、费用的节省是显而易见的,惊人的。
- 因此,维护和升级革命的方式是“瘦”客户机,“胖”服务器。
【2】成本降低,选择更多
- 大家都知道windows在桌面电脑上几乎一统天下,浏览器成为了标准配置
- 但在服务器操作系统上windows并不是处于绝对的统治地位。
- 现在的趋势是凡使用B/S架构的应用管理软件,只需安装在Linux服务器上即可,而且安全性高。
- 所以服务器操作系统的选择是很多的,不管选用那种操作系统都可以让大部分人使用windows作为桌面操作系统电脑不受影响,这就使的最流行免费的Linux操作系统快速发展起来
- Linux除了操作系统是免费的以外,连数据库也是免费的,这种选择非常盛行。
- 比如说很多人每天上“网易”(原文为新浪)网,只要安装了浏览器就可以了,并不需要了解“网易”的服务器用的是什么操作系统,而事实上大部分网站确实没有使用windows操作系统,但用户的电脑本身安装的大部分是windows操作系统。
【3】应用服务器运行数据负荷较重
- 由于B/S架构管理软件只安装在服务器端(Server)上,网络管理人员只需要管理服务器就行了,用户界面主要事务逻辑在服务器(Server)端完全通过WWW浏览器实现,极少部分事务逻辑在前端(Browser)实现
- 所有的客户端只有浏览器,网络管理人员只需要做硬件维护。
- 但是,应用服务器运行数据负荷较重,一旦发生服务器“崩溃”等问题,后果不堪设想。因此,许多单位都备有数据库存储服务器,以防万一。
【6】C/S与B/S区别
- Client/Server是建立在局域网的基础上的
- Browser/Server是建立在广域网的基础上的。
【1】硬件环境不同
- C/S一般建立在专用的网络上
- 小范围里的网络环境,局域网之间再通过专门服务器提供连接和数据交换服务。
- B/S建立在广域网之上的
- 不必是专门的网络硬件环境
- 例如电话上网,租用设备,信息自己管理,有比C/S更强的适应范围
- 一般只要有操作系统和浏览器就行。
【2】对安全要求不同
- C/S一般面向相对固定的用户群,对信息安全的控制能力很强。
- 一般高度机密的信息系统采用C/S结构适宜,可以通过B/S发布部分可公开信息。
- B/S建立在广域网之上,对安全的控制能力相对弱,面向是不可知的用户群。
【3】对程序架构不同
- C/S程序可以更加注重流程,可以对权限多层次校验,对系统运行速度可以较少考虑。
- B/S对安全以及访问速度的多重的考虑,建立在需要更加优化的基础之上。
- 比C/S有更高的要求,B/S结构的程序架构是发展的趋势,从MS的.Net系列的BizTalk2000Exchange2000等,全面支持网络的构件搭建的系统。
- SUN和IBM推的JavaBean构件技术等,使B/S更加成熟。
【4】软件重用不同
- C/S程序可以不可避免的整体性考虑
- 构件的重用性不如在B/S要求下的构件的重用性好。
- B/S对的多重结构,要求构件相对独立的功能。
- 能够相对较好的重用。就如买来的餐桌可以再利用,而不是做在墙上的石头桌子。
【5】系统维护不同
- 系统维护是软件生存周期中,开销大,相当重要
- C/S程序由于整体性,必须整体考察,处理出现的问题以及系统升级难,可能是再做一个全新的系统。
- B/S构件组成方面构件个别的更换,实现系统的无缝升级。
- 系统维护开销减到最小,用户从网上自己下载安装就可以实现升级。
【6】处理问题不同
- C/S程序可以处理用户面固定,并且在相同区域,安全要求高的需求,与操作系统相关,应该都是相同的系统。
- B/S建立在广域网上,面向不同的用户群,分散地域,这是C/S无法作到的,与操作系统平台关系最小。
【7】用户接口不同
- C/S多是建立在Window平台上,表现方法有限,对程序员普遍要求较高。
- B/S建立在浏览器上,有更加丰富和生动的表现方式与用户交流,并且大部分难度减低,降低开发成本。
【8】信息流不同
- C/S程序一般是典型的中央集权的机械式处理,交互性相对低。
- B/S信息流向可变化,B-B、B-C、B-G等信息流向的变化,更象交易中心。
【二】操作系统与网络通信
【1】什么是网络编程
- 网络编程是指通过编程语言在计算机之间建立通信的一种方式。
- 它是在互联网上进行数据传输的关键组成部分,使计算机能够相互通信、交换信息和共享资源。
- 网络编程涉及许多不同的技术和协议,包括TCP/IP(传输控制协议/因特网协议),HTTP(超文本传输协议),FTP(文件传输协议)等。
- 这些协议规定了如何在网络上传输数据以及如何在网络上创建、管理和维护连接。
- 总结:网络编程的研究前提就是基于互联网 网络编程就是基于互联网编写代码
【2】学习网络编程的目的/结果
- 学习网络编程的主要目的是为了开发基于客户端/服务器(C/S)架构的应用程序。
- 这种类型的软件通常由两部分组成:客户端(运行在用户的设备上)和服务器(运行在一台或多台服务器上)。
- 通过学习网络编程,你可以掌握开发C/S应用程序所需的基本原理和技术,并使用各种编程框架来简化开发过程。
- 总结:学习完网络编程之后就可以开发C/S架构的软件(掌握原理 使用框架)
【3】网络编程发展史
(1)网络编程的来源
- 网络编程起源于美国军方的需求,他们希望能够实现不同计算机之间的数据交互。
- 然而,在没有网络编程技术的情况下,他们只能采用物理媒体(如磁带或硬盘)进行数据复制和传输。
- 因此,军方开始研究如何通过计算机网络实现更高效的数据交换,从而诞生了网络编程技术。
- 总结:该技术源于美国军方>>>:很多先进的技术都是由军事发明后续转为民用
(2)实际应用
- 随着计算机硬件的发展和互联网的普及,网络编程逐渐应用于各种领域,包括商业、教育、娱乐、科学研究等。
- 现在,我们可以通过网络编程技术开发各种类型的应用程序,例如网页浏览器、社交媒体平台、在线游戏等。
- 实际中的例子
- 军方想要实现不同计算机之间数据交互
- 没有网络编程技术的时候只能拿U盘拷贝并携带
- 为了跨区域交互数据所以发明了网络编程
【4】早期远程通信
(1)一开始:座机电话
- 彼此打电话需要电话线
- 早期的远程通信主要依赖于座机电话,人们通过拨号将语音信号转换成电信号,然后通过电话线路将这些信号发送到另一端,接收方再将电信号转换回语音信号。
(2)后来:大屁股电脑
- 数据交互需要插网线
- 随着个人计算机的出现,人们可以利用调制解调器(也称为猫)通过电话线路进行数据传输。
- 这种方式被称为异步转移模式(Async Transfer Mode,ATM),虽然速度较慢,但它是最早的宽带网络之一。
(3)现在:智能手机
- 数据交互需要无限网卡
- 远程通信的前提是必须具备一个物理链接介质
- 现在,我们可以通过无线网络(如Wi-Fi、蜂窝数据网络)与互联网进行无缝通信。
- 智能手机已经成为我们生活中不可或缺的一部分,我们可以随时随地访问电子邮件、浏览网页、观看视频、下载音乐等。
【5】互联网协议
- 不同计算机之间要想实现无障碍交互
- 除了需要有物理链接介质之外还需要一套公有的标准
- 互联网协议是用于规范网络通信的标准规则。
- 它们定义了如何在网络上传输数据,以及如何在网络上建立、管理和维护连接。
- 一些常见的互联网协议包括:
- TCP/IP:传输控制协议/因特网协议,是互联网的基础协议,负责确保数据包从源节点安全、可靠地传输到目的地节点。
- HTTP:超文本传输协议,是Web的主要协议,用于在客户端和服务器之间传输HTML文档和其他类型的内容。
- FTP:文件传输协议,用于在计算机之间传输文件。
【6】操作系统
- 操作系统:
- (Operating System,简称OS)是管理和控制计算机硬件与软件资源的计算机程序
- 是直接运行在“裸机”上的最基本的系统软件
- 任何其他软件都必须在操作系统的支持下才能运行。
注:计算机(硬件)->os->应用软件
【7】OSI七层协议介绍
- 互联网协议按照功能不同分为osi七层或tcp/ip五层或tcp/ip四层
- tcp/ip四层
- 应用层
- 传输层
- 网络层
- 网络接口层
- tcp/ip五层
- 应用层
- 传输层
- 网络层
- 数据链路层
- 物理层
- osi七层
- 应用层
- 表示层
- 会话层
- 传输层
- 网络层
- 数据链路层
- 物理层
- 每层运行常见物理设备
- OSI七层协议数据传输的封包与解包过程
【8】tcp/ip五层
- 我们将应用层,表示层,会话层并作应用层,从tcp/ip五层协议的角度来阐述每层的由来与功能,搞清楚了每层的主要协议
- 就理解了整个互联网通信的原理。
- 首先,用户感知到的只是最上面一层应用层,自上而下每层都依赖于下一层,所以我们从最下一层开始切入,比较好理解
- 每层都运行特定的协议,越往上越靠近用户,越往下越靠近硬件
【1】物理层
- 上面提到,孤立的计算机之间要想一起玩,就必须接入internet,言外之意就是计算机之间必须完成组网
- 主要是基于电器特性发送高低电压(电信号),高电压对应数字1,低电压对应数字0
【2】数据链路层
- 单纯的电信号0和1没有任何意义,必须规定电信号多少位一组,每组什么意思
- 定义了电信号的分组方式
【3】网络层
- 有了ethernet、mac地址、广播的发送方式,世界上的计算机就可以彼此通信了,问题是世界范围的互联网是由
- 一个个彼此隔离的小的局域网组成的,那么如果所有的通信都采用以太网的广播方式,那么一台机器发送的包全世界都会收到,
- 这就不仅仅是效率低的问题了,这会是一种灾难
【4】传输层
- 建立端口到端口的通信
- 补充:端口范围0-65535,0-1023为系统占用端口
(1)tcp协议与udp协议
- 可靠传输,TCP数据包没有长度限制,理论上可以无限长
- 但是为了保证网络的效率,通常TCP数据包的长度不会超过IP数据包的长度,以确保单个TCP数据包不必再分割。
- tcp三次握手和四次挥手
(2)udp协议
- 不可靠传输,”报头”部分一共只有8个字节,总长度不超过65,535字节,正好放进一个IP数据包。
以太网头 | ip头 | udp头 | 数据 |
---|---|---|---|
【5】应用层
(1)应用层由来
- 用户使用的都是应用程序,均工作于应用层,互联网是开发的,大家都可以开发自己的应用程序,数据多种多样,必须规定好数据的组织形式
(2)应用层功能
- 规定应用程序的数据格式。
- 例:TCP协议可以为各种各样的程序传递数据
- 比如Email、WWW、FTP等等。
- 那么,必须有不同协议规定电子邮件、网页、FTP数据的格式,这些应用程序协议就构成了”应用层”。
【三】Socket层
- 我们知道两个进程如果需要进行通讯最基本的一个前提能能够唯一的标示一个进程,在本地进程通讯中我们可以使用PID来唯一标示一个进程,但PID只在本地唯一,网络中的两个进程PID冲突几率很大,这时候我们需要另辟它径了,我们知道IP层的ip地址可以唯一标示主机,而TCP层协议和端口号可以唯一标示主机的一个进程,这样我们可以利用ip地址+协议+端口号唯一标示网络中的一个进程。
- 能够唯一标示网络中的进程后,它们就可以利用socket进行通信了,什么是socket呢?
- 我们经常把socket翻译为套接字,socket是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用已实现进程在网络中通信。
【1】什么是socket
- Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。
- 在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面
- 对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
- 所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。
- 也有人将socket说成ip+port
- ip是用来标识互联网中的一台主机的位置
- 而port是用来标识这台机器上的一个应用程序
- ip地址是配置到网卡上的
- 而port是应用程序开启的
- ip与port的绑定就标识了互联网中独一无二的一个应用程序
- 而程序的pid是同一台机器上不同进程或者线程的标识
【2】套接字发展史及分类
- 套接字起源于 20 世纪 70 年代加利福尼亚大学伯克利分校版本的 Unix,即人们所说的 BSD Unix。
- 因此,有时人们也把套接字称为“伯克利套接字”或“BSD 套接字”。
- 一开始,套接字被设计用在同 一台主机上多个应用程序之间的通讯。
- 这也被称进程间通讯,或 IPC。套接字有两种(或者称为有两个种族),分别是
- 基于文件型的
- 基于网络型的。
【1】基于文件类型的套接字家族
- 套接字家族的名字:
- AF_UNIX
- unix一切皆文件
- 基于文件的套接字调用的就是底层的文件系统来取数据
- 两个套接字进程运行在同一机器
- 可以通过访问同一个文件系统间接完成通信
【2】基于网络类型的套接字家族
- 套接字家族的名字:
- AF_INET
- (还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现
- 所有地址家族中,AF_INET是使用最广泛的一个
- python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET)
【3】套接字工作流程
- 一个生活中的场景。
- 你要打电话给一个朋友,先拨号,朋友听到电话铃声后提起电话,这时你和你的朋友就建立起了连接,就可以讲话了。
- 等交流结束,挂断电话结束此次交谈。 生活中的场景就解释了这工作原理。
【1】服务端流程
- 先从服务器端说起。
- 服务器端先初始化Socket
- 然后与端口绑定(bind),对端口进行监听(listen)
- 调用accept阻塞,等待客户端连接。
- 在这时如果有个客户端初始化一个Socket
- 然后连接服务器(connect)
- 如果连接成功,这时客户端与服务器端的连接就建立了。
- 客户端发送数据请求,服务器端接收请求并处理请求
- 然后把回应数据发送给客户端,客户端读取数据
- 最后关闭连接,一次交互结束
- socket()模块函数用法
import socket
socket.socket(socket_family,socket_type,protocal=0)
socket_family 可以是 AF_UNIX 或 AF_INET。socket_type 可以是 SOCK_STREAM 或 SOCK_DGRAM。protocol 一般不填,默认值为 0。
# 获取tcp/ip套接字
tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 获取udp/ip套接字
udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 由于 socket 模块中有太多的属性。我们在这里破例使用了'from module import *'语句。使用 'from socket import *',我们就把 socket 模块里的所有属性都带到我们的命名空间里了,这样能 大幅减短我们的代码。
# 例如tcpSock = socket(AF_INET, SOCK_STREAM
【2】服务端套接字函数
- s.bind() 绑定(主机,端口号)到套接字
- s.listen() 开始TCP监听
- s.accept() 被动接受TCP客户的连接,(阻塞式)等待连接的到来
【3】客户端套接字函数
- s.connect() 主动初始化TCP服务器连接
- s.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
【4】公共用途的套接字函数
- s.recv() 接收TCP数据
- s.send() 发送TCP数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完)
- s.sendall() 发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完)
- s.recvfrom() 接收UDP数据
- s.sendto() 发送UDP数据
- s.getpeername() 连接到当前套接字的远端的地址
- s.getsockname() 当前套接字的地址
- s.getsockopt() 返回指定套接字的参数
- s.setsockopt() 设置指定套接字的参数
- s.close() 关闭套接字
【5】面向锁的套接字方法
- s.setblocking() 设置套接字的阻塞与非阻塞模式
- s.settimeout() 设置阻塞套接字操作的超时时间
- s.gettimeout() 得到阻塞套接字操作的超时时间
【6】面向文件的套接字的函数
- s.fileno() 套接字的文件描述符
- s.makefile() 创建一个与该套接字相关的文件
【4】基于TCP的套接字
【1】方法简介
- tcp是基于链接的
- 必须先启动服务端
- 然后再启动客户端去链接服务端
- tcp服务端
server = socket() #创建服务器套接字
server.bind() #把地址绑定到套接字
server.listen() #监听链接
inf_loop: #服务器无限循环
conn = server.accept() #接受客户端链接
comm_loop: #通讯循环
conn.recv()/conn.send() #对话(接收与发送)
conn.close() #关闭客户端套接字
server.close() #关闭服务器套接字(可选)
- tcp客户端
client = socket() # 创建客户套接字
client.connect() # 尝试连接服务器
comm_loop: # 通讯循环
client.send()/client.recv() # 对话(发送/接收)
client.close() # 关闭客户套接字
【2】打电话模型
- socket通信流程与打电话流程类似
- 我们就以打电话为例来实现一个low版的套接字通信
(1)服务端
import socket
ip_port = ('127.0.0.1', 9000) # 电话卡
BUFSIZE = 1024 # 收发消息的尺寸
servser = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 买手机
servser.bind(ip_port) # 手机插卡
servser.listen(5) # 手机待机
conn, addr = servser.accept() # 手机接电话
# print(conn)
# print(addr)
print('接到来自%s的电话' % addr[0])
msg = conn.recv(BUFSIZE) # 听消息,听话
print(msg, type(msg))
conn.send(msg.upper()) # 发消息,说话
conn.close() # 挂电话
servser.close() # 手机关机
(2)客户端
import socket
ip_port = ('127.0.0.1', 9000)
BUFSIZE = 1024
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect_ex(ip_port) # 拨电话
client.send('dream is handsome'.encode('utf-8')) # 发消息,说话(只能发送字节类型)
feedback = client.recv(BUFSIZE) # 收消息,听话
print(feedback.decode('utf-8'))
client.close() # 挂电话
【3】打电话模型升级版
- 加上链接循环与通信循环
(1)服务端改进版
import socket
# 创建一个套接字对象
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 将套接字绑定到特定的地址和端口
server_address = ('127.0.0.1', 8081)
server_socket.bind(server_address)
# 监听传入的连接请求(队列中最多允许 1 个连接)
server_socket.listen(1)
print('服务器监听在 {}:{}'.format(*server_address))
# 接受连接,阻塞直到接收到连接
connection, client_address = server_socket.accept()
print('接受了来自 {} 的连接'.format(client_address))
while True:
# 接收来自客户端的数据
data = connection.recv(1024)
if not data:
break # 没有更多的数据,退出循环
# 处理接收到的数据
print('接收到的数据:{}'.format(data.decode('utf-8')))
# 向客户端发送响应
response = input('输入响应:')
connection.send(response.encode('utf-8'))
# 清理连接
connection.close()
server_socket.close()
- 客户端改进版
import socket
ip_port = ('127.0.0.1', 8081)
BUFSIZE = 1024
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 拨电话
client.connect_ex(ip_port)
# 新增通信循环,客户端可以不断发收消息
while True:
msg = input('>>: ').strip()
if len(msg) == 0: continue
# 发消息,说话(只能发送字节类型)
client.send(msg.encode('utf-8'))
# 收消息,听话
feedback = client.recv(BUFSIZE)
print(feedback.decode('utf-8'))
# 挂电话
client.close()
【5】基于UDP的套接字
- udp是无链接的,先启动哪一端都不会报错
【1】UDP服务端
server = socket() #创建一个服务器的套接字
server.bind() #绑定服务器套接字
inf_loop: #服务器无限循环
conn = server.recvfrom()/conn.sendto() # 对话(接收与发送)
server.close() # 关闭服务器套接字
【2】UDP客户端
client = socket() # 创建客户套接字
comm_loop: # 通讯循环
client.sendto()/client.recvfrom() # 对话(发送/接收)
client.close() # 关闭客户套接字
演示模板:
【3】UDP服务端
import socket
ip_port = ('127.0.0.1', 9000)
BUFSIZE = 1024
udp_server_client = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
udp_server_client.bind(ip_port)
while True:
msg, addr = udp_server_client.recvfrom(BUFSIZE)
print(msg,addr)
udp_server_client.sendto(msg.upper(),addr)
【4】UDP客户端
import socket
ip_port = ('127.0.0.1',9000)
BUFSIZE = 1024
udp_client_server = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
while True:
msg = input("》》").strip()
if not msg: continue
udp_client_server.sendto(msg.encode('utf-8'),ip_port)
back_msg, addr = udp_client_server.recvfrom(BUFSIZE)
print(back_msg.decode('utf-8'),addr)
【5】QQ聊天模拟
- qq聊天(由于udp无连接,所以可以同时多个客户端去跟服务端通信)
# UDP服务端
import socket
ip_port = ('127.0.0.1',8081)
udp_server_sock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
udp_server_sock.bind(ip_port)
while True:
qq_msg, addr = udp_server_sock.recvfrom(1024)
print('来自[%s:%s]的一条消息:\033[1;44m%s\033[0m' % (addr[0], addr[1], qq_msg.decode('utf-8')))
back_msg = input('回复消息:').strip()
udp_server_sock.sendto(back_msg.encode('utf-8'),addr)
# 客户端一
import socket
BUFSIZE = 1024
udp_client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
qq_name_dic = {
'狗哥alex': ('127.0.0.1', 8081),
'瞎驴': ('127.0.0.1', 8081),
'一棵树': ('127.0.0.1', 8081),
'武大郎': ('127.0.0.1', 8081),
}
while True:
qq_name = input('请选择聊天对象: ').strip()
while True:
msg = input('请输入消息,回车发送: ').strip()
if msg == 'quit': break
if not msg or not qq_name or qq_name not in qq_name_dic: continue
udp_client_socket.sendto(msg.encode('utf-8'), qq_name_dic[qq_name])
back_msg, addr = udp_client_socket.recvfrom(BUFSIZE)
print('来自[%s:%s]的一条消息:\033[1;44m%s\033[0m' % (addr[0], addr[1], back_msg.decode('utf-8')))
udp_client_socket.close()
# 客户端二
import socket
BUFSIZE = 1024
udp_client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
qq_name_dic = {
'狗哥alex': ('127.0.0.1', 8081),
'瞎驴': ('127.0.0.1', 8081),
'一棵树': ('127.0.0.1', 8081),
'武大郎': ('127.0.0.1', 8081),
}
while True:
qq_name = input('请选择聊天对象: ').strip()
while True:
msg = input('请输入消息,回车发送: ').strip()
if msg == 'quit': break
if not msg or not qq_name or qq_name not in qq_name_dic: continue
udp_client_socket.sendto(msg.encode('utf-8'), qq_name_dic[qq_name])
back_msg, addr = udp_client_socket.recvfrom(BUFSIZE)
print('来自[%s:%s]的一条消息:\033[1;44m%s\033[0m' % (addr[0], addr[1], back_msg.decode('utf-8')))
udp_client_socket.close()
【6】基于TCP协议的简单套接字(打电话模型)
【1】初代(一次信息)
(1)服务端
import socket
# 【1】.买手机
# socket.SOCK_STREAM :流式协议 ----> TCP协议 ----> 所有数据是一个整体
# socket.SOCK_DGRAM : 报协议 ----> 每一次数据都是单独一部分
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 【2】.绑定手机卡 ---> 192.168.1.50(本机IP地址)
# 0.0.0.0(任意IP地址) ---> 关联公网IP才会起作用 ----> 服务器用
# 127.0.0.1(本机固定IP地址) ---> 只有本机才能访问这个地址(测试用)
# 端口号:0-65535, 1024以前的都被系统保留使用
# phone.bind(('ip', port))
phone.bind(('127.0.0.1', 8080))
# 【3】.开机 -- 监听状态
# 5 :指的是半连接池的大小
phone.listen(5)
print(f'服务器启动,开始监听ip:>>>{"127.0.0.1"},port:>>>{8080}')
# (1)服务器启动,开始监听ip:>>>127.0.0.1,port:>>>8080
# 【4】.等待电话链接请求:拿到电话链接 conn
# 返回的是双向通路 --- 操作系统维持链接
# conn : 双向通路的链接
# client_addr : 客户端的iP和端口
conn, client_addr = phone.accept()
print('这是服务端的conn:>>>', conn)
# (3)这是服务端的conn:>>> <socket.socket fd=352, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 5249)>
print('这是服务端的client_addr:>>>', client_addr)
# (4)这是服务端的client_addr:>>> ('127.0.0.1', 5249)
# 【5】.接收消息
# 1024 :最大接受的数据量为1024 bytes类型,收到的是bytes类型
data = conn.recv(1024)
# 对接受的二进制数据进行解码
print('从客户端接受的数据:>>>>', data.decode('utf-8'))
# (5)从客户端接受的数据:>>>> is running for 发送信息
# 发消息 返回消息状态等信息
conn.send(data.upper())
# 【6】.关闭连接(必选的回收资源操作) conn
# 完成后断开连接
conn.close()
# 【7】.关机(可选操作)
phone.close()
(2)客户端
import socket
# 【1】.买手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 【2】.拨通服务端电话
# connect(('服务端ip', 服务端端口))
phone.connect(('127.0.0.1', 8080))
# (2)
# 【3】.通信
# send(二进制数据类型)
phone.send('is running for 发送信息'.encode('utf8'))
# 接受服务端返回的数据
data = phone.recv(1024)
# 打印返回的消息 解码
print(data.decode('utf8'))
# (6)IS RUNNING FOR 发送信息
# 【4】.关闭连接(必选的回收资源操作)
phone.close()
(3)问题
信息只能传输一次,无法做到持续发送信息
【2】二代(多次信息-循环结束条件)
(1)服务端
import socket
# 【1】.买手机
# socket.SOCK_STREAM :流式协议 ----> TCP协议 ----> 所有数据是一个整体
# socket.SOCK_DGRAM : 报协议 ----> 每一次数据都是单独一部分
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 【2】.绑定手机卡 ---> 192.168.1.50(本机IP地址)
# 0.0.0.0(任意IP地址) ---> 关联公网IP才会起作用 ----> 服务器用
# 127.0.0.1(本机固定IP地址) ---> 只有本机才能访问这个地址(测试用)
# 端口号:0-65535, 1024以前的都被系统保留使用
# phone.bind(('ip', port))
phone.bind(('127.0.0.1', 8080))
# 【3】.开机 -- 监听状态
# 5 :指的是半连接池的大小
phone.listen(5)
print(f'服务器启动,开始监听ip:>>>{"127.0.0.1"},port:>>>{8080}')
# (1)服务器启动,开始监听ip:>>>127.0.0.1,port:>>>8080
# 【4】.等待电话链接请求:拿到电话链接 conn
# 返回的是双向通路 --- 操作系统维持链接
# conn : 双向通路的链接
# client_addr : 客户端的iP和端口
conn, client_addr = phone.accept()
print('这是服务端的conn:>>>', conn)
# (3)这是服务端的conn:>>> <socket.socket fd=352, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 5249)>
print('这是服务端的client_addr:>>>', client_addr)
# (4)这是服务端的client_addr:>>> ('127.0.0.1', 5249)
while True:
# 【5】.接收消息
# 1024 :最大接受的数据量为1024 bytes类型,收到的是bytes类型
data = conn.recv(1024)
if data.decode('utf-8') == 'q':
break
# 对接受的二进制数据进行解码
print('从客户端接受的数据:>>>>', data.decode('utf-8'))
# (5)从客户端接受的数据:>>>> is running for 发送信息
# 发消息 返回消息状态等信息
conn.send(data.upper())
# 【6】.关闭连接(必选的回收资源操作) conn
# 完成后断开连接
conn.close()
# 【7】.关机(可选操作)
phone.close()
(2)客户端
import socket
# 【1】.买手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 【2】.拨通服务端电话
# connect(('服务端ip', 服务端端口))
phone.connect(('127.0.0.1', 8080))
# (2)
while True:
# 【3】.通信
# send(二进制数据类型)
msg = input('请输入需要发送的消息:>>>>').strip()
phone.send(f'{msg}'.encode('utf8'))
# 加入结束条件强制结束通信
if msg == 'q':
break
# 接受服务端返回的数据
data = phone.recv(1024)
# 打印返回的消息 解码
print(data.decode('utf8'))
# (6)IS RUNNING FOR 发送信息
# 【4】.关闭连接(必选的回收资源操作)
phone.close()
【3】三代(多次信息-信息为空)
(1)服务端
import socket
# 【1】.买手机
# socket.SOCK_STREAM :流式协议 ----> TCP协议 ----> 所有数据是一个整体
# socket.SOCK_DGRAM : 报协议 ----> 每一次数据都是单独一部分
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 【2】.绑定手机卡 ---> 192.168.1.50(本机IP地址)
# 0.0.0.0(任意IP地址) ---> 关联公网IP才会起作用 ----> 服务器用
# 127.0.0.1(本机固定IP地址) ---> 只有本机才能访问这个地址(测试用)
# 端口号:0-65535, 1024以前的都被系统保留使用
# phone.bind(('ip', port))
phone.bind(('127.0.0.1', 8080))
# 【3】.开机 -- 监听状态
# 5 :指的是半连接池的大小
phone.listen(5)
print(f'服务器启动,开始监听ip:>>>{"127.0.0.1"},port:>>>{8080}')
# (1)服务器启动,开始监听ip:>>>127.0.0.1,port:>>>8080
# 【4】.等待电话链接请求:拿到电话链接 conn
# 返回的是双向通路 --- 操作系统维持链接
# conn : 双向通路的链接
# client_addr : 客户端的iP和端口
conn, client_addr = phone.accept()
print('这是服务端的conn:>>>', conn)
# (3)这是服务端的conn:>>> <socket.socket fd=352, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 5249)>
print('这是服务端的client_addr:>>>', client_addr)
# (4)这是服务端的client_addr:>>> ('127.0.0.1', 5249)
while True:
# 【5】.接收消息
# 1024 :最大接受的数据量为1024 bytes类型,收到的是bytes类型
data = conn.recv(1024)
if data.decode('utf-8') == 'q':
break
# 对接受的二进制数据进行解码
print('从客户端接受的数据:>>>>', data.decode('utf-8'))
# (5)从客户端接受的数据:>>>> is running for 发送信息
# 发消息 返回消息状态等信息
conn.send(data.upper())
# 【6】.关闭连接(必选的回收资源操作) conn
# 完成后断开连接
conn.close()
# 【7】.关机(可选操作)
phone.close()
(2)客户端
import socket
# 【1】.买手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 【2】.拨通服务端电话
# connect(('服务端ip', 服务端端口))
phone.connect(('127.0.0.1', 8080))
# (2)
while True:
# 【3】.通信
# send(二进制数据类型)
msg = input('请输入需要发送的消息:>>>>').strip()
phone.send(f'{msg}'.encode('utf8'))
if len(msg) == 0: continue
# 加入结束条件强制结束通信
if msg == 'q':
break
# 接受服务端返回的数据
data = phone.recv(1024)
# 打印返回的消息 解码
print(data.decode('utf8'))
# (6)IS RUNNING FOR 发送信息
# 【4】.关闭连接(必选的回收资源操作)
phone.close()
(3)问题
客户端强制终止程序时,服务端会产生一系列问题
【4】四代(检测用户信息为空)
(1)服务端
import socket
# 【1】.买手机
# socket.SOCK_STREAM :流式协议 ----> TCP协议 ----> 所有数据是一个整体
# socket.SOCK_DGRAM : 报协议 ----> 每一次数据都是单独一部分
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 【2】.绑定手机卡 ---> 192.168.1.50(本机IP地址)
# 0.0.0.0(任意IP地址) ---> 关联公网IP才会起作用 ----> 服务器用
# 127.0.0.1(本机固定IP地址) ---> 只有本机才能访问这个地址(测试用)
# 端口号:0-65535, 1024以前的都被系统保留使用
# phone.bind(('ip', port))
phone.bind(('127.0.0.1', 8080))
# 【3】.开机 -- 监听状态
# 5 :指的是半连接池的大小
phone.listen(5)
print(f'服务器启动,开始监听ip:>>>{"127.0.0.1"},port:>>>{8080}')
# (1)服务器启动,开始监听ip:>>>127.0.0.1,port:>>>8080
# 【4】.等待电话链接请求:拿到电话链接 conn
# 返回的是双向通路 --- 操作系统维持链接
# conn : 双向通路的链接
# client_addr : 客户端的iP和端口
conn, client_addr = phone.accept()
print('这是服务端的conn:>>>', conn)
# (3)这是服务端的conn:>>> <socket.socket fd=352, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 5249)>
print('这是服务端的client_addr:>>>', client_addr)
# (4)这是服务端的client_addr:>>> ('127.0.0.1', 5249)
while True:
# 【5】.接收消息
# 1024 :最大接受的数据量为1024 bytes类型,收到的是bytes类型
try:
data = conn.recv(1024)
if len(data) == 0:
# 在 unix 系统里,一旦data收到的内容为空
# 就意味着一种异常行为:客户端非法断开了链接
break
if data.decode('utf-8') == 'q':
break
# 对接受的二进制数据进行解码
print('从客户端接受的数据:>>>>', data.decode('utf-8'))
# (5)从客户端接受的数据:>>>> is running for 发送信息
# 发消息 返回消息状态等信息
conn.send(data.upper())
except Exception as e:
break
# 【6】.关闭连接(必选的回收资源操作) conn
# 完成后断开连接
conn.close()
# 【7】.关机(可选操作)
phone.close()
(2)客户端
import socket
# 【1】.买手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 【2】.拨通服务端电话
# connect(('服务端ip', 服务端端口))
phone.connect(('127.0.0.1', 8080))
# (2)
while True:
# 【3】.通信
# send(二进制数据类型)
msg = input('请输入需要发送的消息:>>>>').strip()
phone.send(f'{msg}'.encode('utf8'))
if len(msg) == 0: continue
# 加入结束条件强制结束通信
if msg == 'q':
break
# 接受服务端返回的数据
data = phone.recv(1024)
# 打印返回的消息 解码
print(data.decode('utf8'))
# (6)IS RUNNING FOR 发送信息
# 【4】.关闭连接(必选的回收资源操作)
phone.close()
【5】五代(完善版)
(1)说明
服务端应该满足的特点
服务端一直提供服务
在建立连接与结束链接之间再加上一层 循环
服务端并发提供服务
(2)服务端
import socket
# 【1】.买手机
# socket.SOCK_STREAM :流式协议 ----> TCP协议 ----> 所有数据是一个整体
# socket.SOCK_DGRAM : 报协议 ----> 每一次数据都是单独一部分
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 【2】.绑定手机卡 ---> 192.168.1.50(本机IP地址)
# 0.0.0.0(任意IP地址) ---> 关联公网IP才会起作用 ----> 服务器用
# 127.0.0.1(本机固定IP地址) ---> 只有本机才能访问这个地址(测试用)
# 端口号:0-65535, 1024以前的都被系统保留使用
# phone.bind(('ip', port))
phone.bind(('127.0.0.1', 8080))
# 【3】.开机 -- 监听状态
# 5 :指的是半连接池的大小
phone.listen(5)
print(f'服务器启动,开始监听ip:>>>{"127.0.0.1"},port:>>>{8080}')
# (1)服务器启动,开始监听ip:>>>127.0.0.1,port:>>>8080
# 【4】.等待电话链接请求:拿到电话链接 conn
# ---- 加上链接循环 ----> 循环建链接
while True:
# 返回的是双向通路 --- 操作系统维持链接
# conn : 双向通路的链接
# client_addr : 客户端的iP和端口
conn, client_addr = phone.accept()
# (3)这是服务端的conn:>>> <socket.socket fd=352, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 5249)>
print('这是服务端的client_addr:>>>', client_addr)
# (4)这是服务端的client_addr:>>> ('127.0.0.1', 5249)
while True:
# 【5】.接收消息
# 1024 :最大接受的数据量为1024 bytes类型,收到的是bytes类型
try:
data = conn.recv(1024)
if len(data) == 0:
# 在 unix 系统里,一旦data收到的内容为空
# 就意味着一种异常行为:客户端非法断开了链接
break
if data.decode('utf-8') == 'q':
break
# 对接受的二进制数据进行解码
print('从客户端接受的数据:>>>>', data.decode('utf-8'))
# (5)从客户端接受的数据:>>>> is running for 发送信息
# 发消息 返回消息状态等信息
conn.send(data.upper())
except Exception as e:
break
# 【6】.关闭连接(必选的回收资源操作) conn
# 完成后断开连接
conn.close()
# 【7】.关机(可选操作)
phone.close()
(3)客户端
import socket
# 【1】.买手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 【2】.拨通服务端电话
# connect(('服务端ip', 服务端端口))
phone.connect(('127.0.0.1', 8080))
# (2)
while True:
# 【3】.通信
# send(二进制数据类型)
msg = input('请输入需要发送的消息:>>>>').strip()
phone.send(f'{msg}'.encode('utf8'))
if len(msg) == 0: continue
# 加入结束条件强制结束通信
if msg == 'q':
break
# 接受服务端返回的数据
data = phone.recv(1024)
# 打印返回的消息 解码
print(data.decode('utf8'))
# (6)IS RUNNING FOR 发送信息
# 【4】.关闭连接(必选的回收资源操作)
phone.close()
【7】基于UDP协议的简单套接字
【1】UDP协议
- UDP协议 -----> 数据报协议
【2】空数据的处理
- TCP协议是水流式协议:传入的数据不能为空,因为水是一直流的,在传输过程中不会对数据进行操作
- UDP协议是数据报协议:传入的数据可为空,在传输过程中UDP会对数据进行内部的拼接和处理
【3】断开链接的影响
- TCP协议是水流式协议:在建立链接过程中,服务端和客户端的链接是一直存在的,断开一方都会对另一方造成影响
- UDP协议是数据报协议:在建立链接过程中,是通过解析对方数据中的ip和端口,再向另一方返回数据的,所以一方发生问题并不会影响到另一方
【4】模版
(1)服务端
import socket
# 数据报协议 ------> UDP 协议
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 建立链接 ip + 端口
server.bind(('127.0.0.1', 8082))
# 接收到服务端传进来的消息
# 接受收到的 ip 和 端口 对于UDP 来说是非常重要的 因为UDP协议没有建立链接操作
# 返回信息就需要用到拿到的ip和端口
msg_data, clint_addr = server.recvfrom(1024)
msg_data = msg_data.decode("utf8")
IP = clint_addr[0]
PORT = clint_addr[1]
print(f'客户端提供的拿到的消息:>>>>{msg_data}')
print(f'客户端提供的拿到的IP:>>>>{IP}')
print(f'客户端提供的拿到的端口:>>>>{PORT}')
while True:
# 建立链接,返回消息内容
return_msg = msg_data.upper().encode('utf-8')
server.sendto(return_msg, clint_addr)
# 关闭服务
server.close()
(2)客户端
import socket
# 建立socket对象
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
while True:
# 向服务端发送消息
msg = input('msg:>>>>').strip()
server.sendto(f'{msg}'.encode('utf8'), ('127.0.0.1', 8082))
# 接受服务器返回的消息
data_msg, server_addr = server.recvfrom(1024)
msg_data = data_msg.decode("utf8")
IP = server_addr[0]
PORT = server_addr[1]
print(f'服务端返回拿到的消息:>>>>{data_msg}')
print(f'服务端返回拿到的IP:>>>>{IP}')
print(f'服务端返回拿到的端口:>>>>{PORT}')
# 关闭链接
server.close()
【5】应用(聊天室)
- 客户端输入消息
- 将数据上传到服务器
- 服务器解析收到的数据,根据接收到的数据进行返回数据
【四】粘包问题
【1】什么是粘包
(1)socket收发消息的原理
- 须知:只有TCP有粘包现象,UDP永远不会粘包
- 发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据
- 也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的
- 因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。
- 而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。
(2)如何定义消息
- 可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
- 例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
- 此外
- 发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率
- 发送方往往要收集到足够多的数据后才发送一个TCP段。
- 若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
(3)TCP
- TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。
- 收发两端(客户端和服务器端)都要有一一成对的socket
- 因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。
- 这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
(4)UDP
- UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。
- 不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息)
- 这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
【2】什么是粘包问题
- 客户端发送需要执行的代码
- 服务端接收到客户端传过来的代码
- 服务端调用方法执行代码并拿到执行后的结果
- 服务端将执行后的结果进行返回
- 客户端接收到服务端返回的结果并做打印输出
# 服务端
# 客户端设置的 ip 和 port
from socket import *
# 执行命令模块
import subprocess
# 创建服务对象
server = socket(AF_INET, SOCK_STREAM)
# 建立链接桥梁
server.bind(('127.0.0.1', 8080))
# 指定半连接池大小
server.listen(5)
# 接收数据和发送数据
while True:
# 从半连接池里面取出链接请求,建立双向链接,拿到连接对象
# 拿到 建成的链接对象 和 客户端的 ip port
conn, client_addr = server.accept()
while True:
# 检测可能会抛出的异常 并 对异常做处理
try:
# 基于 取出的链接对象 进行通信
cmd_from_client = conn.recv(1024)
# 不允许传过来的信息为空
if len(cmd_from_client) == 0:
break
# 执行客户端传过来的命令
# 接收执行命令的结果
msg_server = subprocess.Popen(cmd_from_client.decode('utf-8'), # 对命令进行解码
shell=True, # 执行shell命令
stdout=subprocess.PIPE, # 管道一
stderr=subprocess.PIPE, # 管道二
)
# 返回命令的结果 ---- 成功或失败 (Linux系统可以用utf-8解码,Windows系统需要用gbk解码)
true_msg = msg_server.stdout.read() # 读取到执行成功的结果 ---- 二进制数据类型
false_msg = msg_server.stderr.read() # 读取到执行失败的结果 ---- 二进制数据类型
# 反馈信息给 发送信息的客户端
conn.send(true_msg)
conn.send(false_msg)
except Exception as e:
break
conn.close()
# 客户端
from socket import *
# 创建 socket 对象
client = socket(AF_INET, SOCK_STREAM)
# 创建链接 IP 和 端口
client.connect(('127.0.0.1', 8080))
while True:
msg = input('enter msg :>>>').strip()
# 输入的内容不能为空
if len(msg) == 0: continue
# 传输过程中的数据为二进制数据。对文本数据进行转码
msg = msg.encode('utf-8')
client.send(msg)
# 接收来自服务端返回的结果
msg_from_server = client.recv(1024)
# 对服务端返回的信息进行解码(Mac/Linux解码用utf-8,Windows用GBK)
msg_from_server = msg_from_server.decode('gbk')
print(msg_from_server)
client.close()
【3】UDP协议不存在粘包问题
-
粘包问题出现的原因
- TCP 协议是流式协议,数据像水流一样粘在一起,没有任何边界之分
- 收数据没有接收干净,有残留,就会和下一次的结果混淆在一起
-
解决粘包问题的核心法门就是
- 每次都收干净
- 不造成数据的混淆
-
当我们启动udp服务端后,由udp客户端向服务端发送两条数据
-
但是在udp服务端只接收到了一条数据
-
这是因为 udp 是报式协议,传送数据过程中会将数据打包直接发走,不会对数据进行拼接操作(没有Nagle算法)
【4】TCP协议解决粘包问题基础
【1】解决思路
- 利用
struct
模块将传输过去的数据的总长度 打包 + 到头部进行发送
【2】服务端
# client.connect(('127.0.0.1', 8880))
# 客户端设置的 ip 和 port
from socket import *
# 执行命令模块
import subprocess
# 将数据打包成指定4个长度的数据
import struct
# 创建服务对象
server = socket(AF_INET, SOCK_STREAM)
# 建立链接桥梁
server.bind(('127.0.0.1', 8083))
# 指定半连接池大小
server.listen(5)
# 接收数据和发送数据
while True:
# 从半连接池里面取出链接请求,建立双向链接,拿到连接对象
# 拿到 建成的链接对象 和 客户端的 ip port
conn, client_addr = server.accept()
while True:
# 检测可能会抛出的异常 并 对异常做处理
try:
# 基于 取出的链接对象 进行通信
cmd_from_client = conn.recv(1024)
# 不允许传过来的信息为空
if len(cmd_from_client) == 0:
break
# 执行客户端传过来的命令
# 接收执行命令的结果
msg_server = subprocess.Popen(cmd_from_client.decode('utf-8'), # 对命令进行解码
shell=True, # 执行shell命令
stdout=subprocess.PIPE, # 管道一
stderr=subprocess.PIPE, # 管道二
)
# 返回命令的结果 ---- 成功或失败 (Linux系统可以用utf-8解码,Windows系统需要用gbk解码)
true_msg = msg_server.stdout.read() # 读取到执行成功的结果 ---- 二进制数据类型
false_msg = msg_server.stderr.read() # 读取到执行失败的结果 ---- 二进制数据类型
# (1):先发头部信息(固定长度的bytes二进制数据):对数据信息的描述(包括数据的总长度)
total_size_from_server = len(true_msg) + len(false_msg)
# int类型 -----> 固定长度的 bytes
# 参数 i 表示是整型,具体解释参考文档
total_size_from_server_pack = struct.pack('i', total_size_from_server)
conn.send(total_size_from_server_pack)
# 反馈信息给 发送信息的客户端
conn.send(true_msg)
conn.send(false_msg)
except Exception as e:
break
conn.close()
【3】客户端
from socket import *
# 解指定数据长度
import struct
# 创建 socket 对象
client = socket(AF_INET, SOCK_STREAM)
# 创建链接 IP 和 端口
client.connect(('127.0.0.1', 8083))
while True:
msg = input('enter msg :>>>').strip()
# 输入的内容不能为空
if len(msg) == 0:
continue
# 传输过程中的数据为二进制数据。对文本数据进行转码
msg = msg.encode('utf-8')
client.send(msg)
# 接收来自服务端返回的结果
# 解决粘包问题:解决办法
# (1) 先收到固定长度的头,将头部解析到数据的描述信息,拿到数据的总大小 recv_total_size
# 解析出接收到的总数据的长度
recv_total_size_msg = client.recv(4)
# 解包返回的是元祖。元祖第一个参数就是打包的数字
recv_total_size = struct.unpack('i', recv_total_size_msg)[0]
# (2) recv_size = 0 ,循环接收,每接收一次,recv_size += 接收的长度
# (3) 直到 recv_size = recv_total_size 表示接受信息完毕,结束循环
# 初始化数据长度
recv_size = 0
while recv_size < recv_total_size:
# 本次接收 最多能接收 1024 字节的数据
msg_from_server = client.recv(1024)
# 本次接收到的打印的数据长度
recv_size += len(msg_from_server)
# 对服务端返回的信息进行解码(Mac/Linux解码用utf-8,Windows用GBK)
msg_from_server = msg_from_server.decode('gbk')
print(msg_from_server, end='')
else:
print('命令结束')
client.close()
- 客户端可以完美的接收到查出额定长度以外的数据
- 同时这也是 自定义协议的 简单操作
【5】TCP协议解决粘包问题进阶
- 通过json模式 ---- 模版修改参数直接套用
【1】服务端
# client.connect(('127.0.0.1', 8880))
# 客户端设置的 ip 和 port
from socket import *
# 执行命令模块
import subprocess
# 将数据打包成指定4个长度的数据
import struct
# 将头部信息转成json格式(通用信息格式)
import json
# 创建服务对象
server = socket(AF_INET, SOCK_STREAM)
# 建立链接桥梁
server.bind(('127.0.0.1', 8085))
# 指定半连接池大小
server.listen(5)
# 接收数据和发送数据
while True:
# 从半连接池里面取出链接请求,建立双向链接,拿到连接对象
# 拿到 建成的链接对象 和 客户端的 ip port
conn, client_addr = server.accept()
while True:
# 检测可能会抛出的异常 并 对异常做处理
try:
# 基于 取出的链接对象 进行通信
cmd_from_client = conn.recv(1024)
# 不允许传过来的信息为空
if len(cmd_from_client) == 0:
break
# 执行客户端传过来的命令
# 接收执行命令的结果
msg_server = subprocess.Popen(cmd_from_client.decode('utf-8'), # 对命令进行解码
shell=True, # 执行shell命令
stdout=subprocess.PIPE, # 管道一
stderr=subprocess.PIPE, # 管道二
)
# 返回命令的结果 ---- 成功或失败 (Linux系统可以用utf-8解码,Windows系统需要用gbk解码)
true_msg = msg_server.stdout.read() # 读取到执行成功的结果 ---- 二进制数据类型
false_msg = msg_server.stderr.read() # 读取到执行失败的结果 ---- 二进制数据类型
# (1):先发头部信息(固定长度的bytes二进制数据):对数据信息的描述(包括数据的总长度)
total_size_from_server = len(true_msg) + len(false_msg)
# (2)自定义头部信息
headers_dict = {
'file_name': 'a.txt',
'total_size': total_size_from_server,
'md5': 'md5'
}
# 打包头部信息 - 将字典转成 json 格式数据类型
json_data_str = json.dumps(headers_dict)
# 将 json 格式数据转成二进制数据传输
json_data_bytes = json_data_str.encode('utf-8')
# int类型 -----> 将json格式的二进制数据打成固定长度的 bytes
# 参数 i 表示是整型,具体解释参考文档
json_data_size_pack = struct.pack('i', len(json_data_bytes))
conn.send(json_data_size_pack)
# 发送打包好的头信息
conn.send(json_data_bytes)
# 反馈信息给 发送信息的客户端
conn.send(true_msg)
conn.send(false_msg)
except Exception as e:
break
conn.close()
【2】客户端
import json
from socket import *
# 解指定数据长度
import struct
# 创建 socket 对象
client = socket(AF_INET, SOCK_STREAM)
# 创建链接 IP 和 端口
client.connect(('127.0.0.1', 8085))
while True:
msg = input('enter msg :>>>').strip()
# 输入的内容不能为空
if len(msg) == 0:
continue
# 传输过程中的数据为二进制数据。对文本数据进行转码
msg = msg.encode('utf-8')
client.send(msg)
# 接收来自服务端返回的结果
# (1.1) 先收四个字节的数据,从接收到的数据中解析出json格式的二进制数据的长度
json_data_size_unpack = client.recv(4)
# 解包返回的是元祖。元祖第一个参数就是打包的数字
json_data_size = struct.unpack('i', json_data_size_unpack)[0]
# (1.2) 对 服务端 返回的数据中指定长度进行截取 拿到 json 格式的二进制数据
json_data_bytes = client.recv(json_data_size)
# (1.3) 对指定数据进行json格式的解码并取出需要的信息
header_dict_str = json_data_bytes.decode('utf-8')
header_dict = json.loads(header_dict_str)
# (1.4) 取出字典中的信息总长度
recv_total_size = header_dict['total_size']
# (2) 接收真实的数据
# recv_size = 0 ,循环接收,每接收一次,recv_size += 接收的长度
# (3) 直到 recv_size = recv_total_size 表示接受信息完毕,结束循环
# 初始化数据长度
recv_size = 0
while recv_size < recv_total_size:
# 本次接收 最多能接收 1024 字节的数据
msg_from_server = client.recv(1024)
# 本次接收到的打印的数据长度
recv_size += len(msg_from_server)
# 对服务端返回的信息进行解码(Mac/Linux解码用utf-8,Windows用GBK)
msg_from_server = msg_from_server.decode('gbk')
print(msg_from_server, end='')
else:
print('命令结束')
client.close()
【补充】struct模块
-
struct.pack()
是Python内置模块
struct
中的一个函数
- 它的作用是将指定的数据按照指定的格式进行打包,并将打包后的结果转换成一个字节序列(byte string)
- 可以用于在网络上传输或者储存于文件中。
-
struct.pack(fmt, v1, v2, ...)
- 其中,
fmt
为格式字符串,指定了需要打包的数据的格式,后面的v1
,v2
,...则是需要打包的数据。 - 这些数据会按照
fmt
的格式被编码成二进制的字节串,并返回这个字节串。
- 其中,
-
fmt
的常用格式符如下:
x
--- 填充字节c
--- char类型,占1字节b
--- signed char类型,占1字节B
--- unsigned char类型,占1字节h
--- short类型,占2字节H
--- unsigned short类型,占2字节i
--- int类型,占4字节I
--- unsigned int类型,占4字节l
--- long类型,占4字节(32位机器上)或者8字节(64位机器上)L
--- unsigned long类型,占4字节(32位机器上)或者8字节(64位机器上)q
--- long long类型,占8字节Q
--- unsigned long long类型,占8字节f
--- float类型,占4字节d
--- double类型,占8字节s
--- char[]类型,占指定字节个数,需要用数字指定长度p
--- char[]类型,跟s
一样,但通常用来表示字符串?
--- bool类型,占1字节
-
具体的格式化规则可以在Python文档中查看(链接)。
import json
import struct
# 为避免粘包,必须自定制报头
# 1T数据,文件路径和md5值
header = {'file_size': 1073741824000, 'file_name': '/a/b/c/d/e/a.txt',
'md5': '8f6fbf8347faa4924a76856701edb0f3'}
# 为了该报头能传送,需要序列化并且转为bytes
# 序列化并转成bytes,用于传输
head_bytes = bytes(json.dumps(header), encoding='utf-8')
# 为了让客户端知道报头的长度,用struck将报头长度这个数字转成固定长度:4个字节
# 这4个字节里只包含了一个数字,该数字是报头的长度
head_len_bytes = struct.pack('i', len(head_bytes))
print(f"这是原本的数据 :>>>> {header}")
print(f"这是json序列化后的数据 :>>>> {head_bytes}")
print(f"这是压缩后的数据 :>>>> {head_len_bytes}")
# 这是原本的数据 :>>>> {'file_size': 1073741824000, 'file_name': '/a/b/c/d/e/a.txt', 'md5': '8f6fbf8347faa4924a76856701edb0f3'}
# 这是json序列化后的数据 :>>>> b'{"file_size": 1073741824000, "file_name": "/a/b/c/d/e/a.txt", "md5": "8f6fbf8347faa4924a76856701edb0f3"}'
# 这是压缩后的数据 :>>>> b'h\x00\x00\x00'
FTP文件传输器
# 服务端
import socket
import struct
import json
import subprocess
import os
class MYTCPServer:
address_family = socket.AF_INET
socket_type = socket.SOCK_STREAM
allow_reuse_address = False
max_packet_size = 8192
coding='utf-8'
request_queue_size = 5
server_dir='file_upload'
def __init__(self, server_address, bind_and_activate=True):
"""Constructor. May be extended, do not override."""
self.server_address=server_address
self.socket = socket.socket(self.address_family,
self.socket_type)
if bind_and_activate:
try:
self.server_bind()
self.server_activate()
except:
self.server_close()
raise
def server_bind(self):
"""Called by constructor to bind the socket.
"""
if self.allow_reuse_address:
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.socket.bind(self.server_address)
self.server_address = self.socket.getsockname()
def server_activate(self):
"""Called by constructor to activate the server.
"""
self.socket.listen(self.request_queue_size)
def server_close(self):
"""Called to clean-up the server.
"""
self.socket.close()
def get_request(self):
"""Get the request and client address from the socket.
"""
return self.socket.accept()
def close_request(self, request):
"""Called to clean up an individual request."""
request.close()
def run(self):
while True:
self.conn,self.client_addr=self.get_request()
print('from client ',self.client_addr)
while True:
try:
head_struct = self.conn.recv(4)
if not head_struct:break
head_len = struct.unpack('i', head_struct)[0]
head_json = self.conn.recv(head_len).decode(self.coding)
head_dic = json.loads(head_json)
print(head_dic)
#head_dic={'cmd':'put','filename':'a.txt','filesize':123123}
cmd=head_dic['cmd']
if hasattr(self,cmd):
func=getattr(self,cmd)
func(head_dic)
except Exception:
break
def put(self,args):
file_path=os.path.normpath(os.path.join(
self.server_dir,
args['filename']
))
filesize=args['filesize']
recv_size=0
print('----->',file_path)
with open(file_path,'wb') as f:
while recv_size < filesize:
recv_data=self.conn.recv(self.max_packet_size)
f.write(recv_data)
recv_size+=len(recv_data)
print('recvsize:%s filesize:%s' %(recv_size,filesize))
tcpserver1=MYTCPServer(('127.0.0.1',8080))
tcpserver1.run()
# 客户端
import socket
import struct
import json
import os
class MYTCPClient:
address_family = socket.AF_INET
socket_type = socket.SOCK_STREAM
allow_reuse_address = False
max_packet_size = 8192
coding='utf-8'
request_queue_size = 5
def __init__(self, server_address, connect=True):
self.server_address=server_address
self.socket = socket.socket(self.address_family,
self.socket_type)
if connect:
try:
self.client_connect()
except:
self.client_close()
raise
def client_connect(self):
self.socket.connect(self.server_address)
def client_close(self):
self.socket.close()
def run(self):
while True:
inp=input(">>: ").strip()
if not inp:continue
l=inp.split()
cmd=l[0]
if hasattr(self,cmd):
func=getattr(self,cmd)
func(l)
def put(self,args):
cmd=args[0]
filename=args[1]
if not os.path.isfile(filename):
print('file:%s is not exists' %filename)
return
else:
filesize=os.path.getsize(filename)
head_dic={'cmd':cmd,'filename':os.path.basename(filename),'filesize':filesize}
print(head_dic)
head_json=json.dumps(head_dic)
head_json_bytes=bytes(head_json,encoding=self.coding)
head_struct=struct.pack('i',len(head_json_bytes))
self.socket.send(head_struct)
self.socket.send(head_json_bytes)
send_size=0
with open(filename,'rb') as f:
for line in f:
self.socket.send(line)
send_size+=len(line)
print(send_size)
else:
print('upload successful')
client=MYTCPClient(('127.0.0.1',8080))
client.run()
将一个视频做成服务端和客户端 客户端可以输入文件地址带着文件名 发送给服务端,服务端根据指定的文件地址把视频文件二进制数据读出来发给客户端 客户端可以指定文件保存位置 将数据保存到本地 用到struct json os socket
# 服务端
import socket
import os
import struct
import json
def send_video(conn, file_path):
# 获取文件大小
file_size = os.path.getsize(file_path)
file_name = os.path.basename(file_path)
# 使用 struct 将文件大小打包成 8 字节的二进制数据
packed_size = struct.pack('Q', file_size)
# 使用 JSON 序列化文件名
file_info = {'name': file_name, 'size': file_size}
packed_info = json.dumps(file_info).encode('utf-8')
# 发送打包后的文件信息给客户端
conn.send(packed_info)
# 发送打包后的文件大小给客户端
conn.send(packed_size)
# 打开文件并发送二进制数据
with open(file_path, 'rb') as f:
data = f.read(1024)
while data:
conn.send(data)
data = f.read(1024)
def main():
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_address = ('127.0.0.1', 8082)
server_socket.bind(server_address)
server_socket.listen(1)
print('等待客户端连接...')
conn, client_address = server_socket.accept()
print('接受了来自 {} 的连接'.format(client_address))
file_path = input('请输入要传输的视频文件路径:')
try:
send_video(conn, file_path)
print('文件传输完成')
except Exception as e:
print('文件传输失败:', e)
conn.close()
server_socket.close()
if __name__ == '__main__':
main()
# 客户端
import socket
import struct
import json
import os
def receive_video(conn, save_path):
# 接收文件信息
packed_info = conn.recv(1024)
file_info = json.loads(packed_info.decode('utf-8'))
# 获取文件大小
file_size = file_info['size']
file_name = file_info['name']
print('接收到文件信息:', file_info)
# 使用 struct 解包文件大小
packed_size = conn.recv(8)
file_size = struct.unpack('Q', packed_size)[0]
# 接收文件数据
with open(os.path.join(save_path, file_name), 'wb') as f:
received_size = 0
while received_size < file_size:
data = conn.recv(1024)
f.write(data)
received_size += len(data)
print('\r已接收: {} / {} 字节'.format(received_size, file_size), end='', flush=True)
def main():
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_address = ('127.0.0.1', 8082)
client_socket.connect(server_address)
save_path = input('请输入保存视频文件的路径:')
if not os.path.exists(save_path):
os.makedirs(save_path)
try:
receive_video(client_socket, save_path)
print('\n文件接收完成')
except Exception as e:
print('\n文件接收失败:', e)
client_socket.close()
if __name__ == '__main__':
main()
标签:socket,编程,网络,server,client,服务端,msg,conn
From: https://www.cnblogs.com/Fredette/p/17968044