一、前言
我们现在已经学习了python基础编程,我们知道在本机中,可以编写两个程序,通过读写同一主机磁盘中固定内存,可以实现两个程序之间的通信,单机程序通信过程如下:
而如果两台主机之间要实现通信,就需要网络编程。
网络通信就像寄信件,是信息与数据的交换,而在生活中我们寄信件,信件也不是从我们手里瞬间到收件人手里,每一次信件通信,都会经历这样几个固定流程:写信、装信封、投到邮箱、邮局取件、运输到目的地邮局、目的地邮局根据详细地址派送、收件人收件、拆信封、读信。
网络通信也是同样的道理,数据的传输总有一定的流程:发送端程序将数据打包,给数据包印上目标地址,将数据包交给网关,通过路由转发到达目的网络,目的网络网关在根据详细地址分发、目的主机接收数据、拆包、读数据。
二.软件开发的架构
我们了解的涉及到两个程序之间通讯的应用大致可以分为两种:
第一种是应用类:qq、微信、网盘、优酷这一类是属于需要安装的桌面应用
第二种是web类:比如百度、知乎、博客园等使用浏览器访问就可以直接使用的应用
这些应用的本质其实都是两个程序之间的通讯。而这两个分类又对应了两个软件开发的架构~
1.C/S架构
C/S即:Client与Server ,中文意思:客户端与服务器端架构,这种架构也是从用户层面(也可以是物理层面)来划分的。
这里的客户端一般泛指客户端应用程序EXE,程序需要先安装后,才能运行在用户的电脑上,对用户的电脑操作系统环境依赖较大。
2.B/S架构
B/S即:Browser与Server,中文意思:浏览器端与服务器端架构,这种架构是从用户层面来划分的。
Browser浏览器,其实也是一种Client客户端,只是这个客户端不需要大家去安装什么应用程序,只需在浏览器上通过HTTP请求服务器端相关的资源(网页资源),客户端Browser浏览器就能进行增删改查。

三、网络编程理论基础
网络协议处理 + ip 地址标注 + 端口限制 ——>>>形成成熟的互联网计算机系统
3.1 网络概述
3.1.1 网络的由来
早期的计算机都是独立的一台一台以数据运算为主的机器,随着时代的发展,运算的数据的共享需求变得迫切,为了解决这一个问题,就有了网络的产生,通过物理路径(有线网或无线网)将多台计算机连接起来组成一个互联网计算机平台,实现多台计算机之间特定的数据交互模式。
3.1.2 IP地址的由来
和作用在计算机可以进行数据交互的基础上,又出现了新问题,如果主机要与某一台主机进行通信,如果所连接的主机数量少的情况下,可以通过对主机进行编号来识别要发送给哪一台主机,但是如果连接数量过大时,就是出现数据溢出或错误的情况的,给数据交互带来很大的麻烦。为了解决这个问题,能够在众多计算机中找到特定的计算机,int cerf 在实验室模拟阶段使用了 32 位标记的网络地址协议[internet protocal],用于标识网络上唯一的一台计算机,也就是后来的 ip 地址;
根据 ip 地址,按照网络主机数量【限制】,网络可以分为:
1. 局域网
2. 城域网
3. 广域网—>>全球网络
3.1.3 端口port的由来
和作用在通过 IP 地址确定了网络上的某个具体主机之后,具体的数据通信主要是通过工作在计算机中的软件执行的,不同的软件通信很容易造成问题,那么问题又来了,随着计算行业的发展,计算机中会安装各种各样的应用软件,当主机A向主机B发送信息时,具体会由主机B上的哪个程序接收呢?由此计算机中出现了端口port的概念,端口port主要用于区别不同软件的通信渠道,用于正确的将数据通过指定的端口渠道传输给对应的软件!
3.1.4 网络协议
网络协议的发展过程中,比较混乱,所以有一个非盈利性质的民间组织推出了网络协议公共标准,任何计算机的厂商,制造的计算机必须符合这样的标准才允许上市。
这个组织称为:国际标准化组织/欧洲计算机制造协会联盟组织/ISO。
ISO组织推出了网络协议标准模型:开放互联系统模型(OSI/RM模型)
网络协议处理 + ip 地址标注 + 端口限制 ——>>>形成成熟的互联网计算机系统
3.2 IP&PORT
首先,程序必须要启动,其次,必须有这台机器的地址,我们都知道我们人的地址大概就是国家\省\市\区\街道\楼\门牌号这样字。那么每一台联网的机器在网络上也有自己的地址,它的地址是怎么表示的呢?
3.2.1 ip地址
IP 地址根据使用的用户性质,主要分为 5 类 IP 地址(A类、B类、C类、D类、E类)
IP地址由两部分组成,即网络地址和主机地址。网络地址表示其属于互联网的哪一个网络,主机地址表示其属于该网络中的哪一台主机。二者是主从关系。网络地址和主机地址的区分主要通过子网掩码进行划分
IP地址的四大类型标识的是网络中的某台主机。IPv4的地址长度为32位,共4个字节,但实际中我们用点分十进制记法。
图 3.1 五类互联网地址
IP地址根据网络号和主机号来分,分为A、B、C三类及特殊地址D、E。 全0和全1的都保留不用。
A类:(1.0.0.0-126.0.0.0)(默认子网掩码:255.0.0.0或 0xFF000000)第一个字节为网络号,后三个字节为主机号。该类IP地址的最前面为“0”,所以地址的网络号取值于1~126之间。一般用于大型网络。
B类:(128.0.0.0-191.255.0.0)(默认子网掩码:255.255.0.0或0xFFFF0000)前两个字节为网络号,后两个字节为主机号。该类IP地址的最前面为“10”,所以地址的网络号取值于128~191之间。一般用于中等规模网络。
C类:(192.0.0.0-223.255.255.0)(子网掩码:255.255.255.0或 0xFFFFFF00)前三个字节为网络号,最后一个字节为主机号。该类IP地址的最前面为“110”,所以地址的网络号取值于192~223之间。一般用于小型网络。
D类:是多播地址。该类IP地址的最前面为“1110”,所以地址的网络号取值于224~239之间。一般用于多路广播用户 。
E类:是保留地址。该类IP地址的最前面为“1111”,所以地址的网络号取值于240~255之间。
在IP地址3种主要类型里,各保留了3个区域作为私有地址,其地址范围如下:
A类地址:10.0.0.0~10.255.255.255
B类地址:172.16.0.0~172.31.255.255
C类地址:192.168.0.0~192.168.255.255
回送地址:127.0.0.1。 也是本机地址,等效于localhost或本机IP。一般用于测试使用。例如:ping 127.0.0.1来测试本机TCP/IP是否正常。
IP地址和子网掩码求与运算就可以获得网络地址,确定该子网在互联网中的身份。主机地址可以确定该子网中具体的主机。
3.2.2 "端口"是英文port的意译,可以认为是设备与外界通讯交流的出口
在通过 IP 地址确定了网络上的某个具体主机之后,具体的数据通信主要是通过工作在计算机中的软件执行的,不同的软件通信很容易造成问题。
不同的端口号分类
计算机中的端口号的范围是 0 ~ 65535 之间
端口好根据使用场景,一般区分为公用端口/动态端口/保留端口
公用端口:0 ~ 1023
动态端口:1024 ~ 65535
保留端口:一般是UNIX系统中超级用户进程分配保留端口号
3.3 协议
3.3.1 协议
如果你要跟别人一起玩,那你就需要上网了,什么是互联网?
互联网的核心就是由一堆协议组成,协议就是标准,比如全世界人通信的标准是英语,如果把计算机比作人,互联网协议就是计算机界的英语。所有的计算机都学会了互联网协议,那所有的计算机都就可以按照统一的标准去收发信息从而完成通信了。
3.3.2 osi七层模型
人们按照分工不同把互联网协议从逻辑上划分了层级:

分层:
应用层 (Application): 网络服务与最终用户的一个接口。
协议有:HTTP FTP TFTP SMTP SNMP DNS TELNET HTTPS POP3 DHCP
表示层(Presentation Layer): 数据的表示、安全、压缩。(在五层模型里面已经合并到了应用层)
格式有,JPEG、ASCll、DECOIC、加密格式等
会话层(Session Layer): 建立、管理、终止会话。(在五层模型里面已经合并到了应用层)
对应主机进程,指本地主机与远程主机正在进行的会话
传输层 (Transport): 定义传输数据的协议端口号,以及流控和差错校验。
协议有:TCP UDP,数据包一旦离开网卡即进入网络传输层
网络层 (Network): 进行逻辑地址寻址,实现不同网络之间的路径选择。
协议有:ICMP IGMP IP(IPV4 IPV6) ARP RARP
数据链路层 (Link): 建立逻辑连接、进行硬件地址寻址、差错校验等功能。(由底层网络定义协议)
将比特组合成字节进而组合成帧,用MAC地址访问介质,错误发现但不能纠正。
物理层(Physical Layer): 建立、维护、断开物理连接。(由底层网络定义协议)
3.3.3 协议簇
协议通常指代单独的一个协议,协议族通常指代互相关联的一组协议,协议栈指代某一组互相关联的协议和他们所属的 OSI 模型的层级结构。
四、socket编程
4.1 socket层

图 4.1 socket层是介于应用层和传输层之间
理解socket:Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。
注意:
1 将socket说成ip+port,ip是用来标识互联网中的一台主机的位置,而port是用来标识这台机器上的一个应用程序,ip地址是配置到网卡上的,而port是应用程序开启的,ip与port的绑定就标识了互联网中独一无二的一个应用程序
2 而程序的pid是同一台机器上不同进程或者线程的标识(Google Chrome会有多个PID)
4.2 套接字
4.2.1 套接字的发展历史和分类
套接字家族的名字:AF_UNIX
unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信
基于网络类型的套接字家族
套接字家族的名字:AF_INET
(还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我们只使用AF_INET)
4.2.2 语法
套接字模型对象,为了能明确的表示网络中一台数据交互的主机,需要通过 IP 地址寻址确定主机位置,通过 PORT 端口号确定主机交互接口.
在网络套接字交互过程中,出现了两种类型的套接字模型:
a.面向连接的套接字模型
b.面向无连接的套接字模型
python 中提供的网络套接字,主要包含在 socket 模块中,socket模块提供了socket函数创建套接字。
基本语法结构:
基本语法:
socket.socket(socket_family, socket_type, protocal=0)
参数:
socket_family: socket 地址家族, AF_UNIX/AF_LOCAL 或者 AF_INET
socket_type: socket 连接类型:1)面向连接的(SOCK_STREAM) 2)面向无连接的(SOCK_DGRAME)
protocal:传输协议,一般不用设置,使用默认值进行自动匹配就好
4.3 tcp协议和udp协议
TCP(Transmission Control Protocol)可靠的、面向连接的协议(eg:打电话)、传输效率低全双工通信(发送缓存&接收缓存)、面向字节流。使用TCP的应用:Web浏览器;电子邮件、文件传输程序。
UDP(User Datagram Protocol)不可靠的、无连接的服务,传输效率高(发送前时延小),一对一、一对多、多对一、多对多、面向报文,尽最大努力服务,无拥塞控制。使用UDP的应用:域名系统 (DNS);视频流;IP语音(VoIP)。

4.4 实战演示
关于常用函数简介:
socket()模块函数用法:
创建socket对象socket.socket(socket_family, socket_type, protocal=0)
获取tcp/ip套接字获取udp/ip套接字
udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
服务端套接字函数
s.bind() 绑定(主机,端口号)到套接字
s.listen() 开始TCP监听
s.accept() 被动接受TCP客户的连接,(阻塞式)等待连接的到来
客户端套接字函数
s.connect() 主动初始化TCP服务器连接
s.connect_ex() connect() 函数的扩展版本,出错时返回出错码,而不是抛出异常
公共用途的套接字函数
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() 关闭套接字
面向锁的套接字方法
s.setblocking() 设置套接字的阻塞与非阻塞模式
s.settimeout() 设置阻塞套接字操作的超时时间
s.gettimeout() 得到阻塞套接字操作的超时时间
面向文件的套接字的函数
s.fileno() 套接字的文件描述符
s.makefile() 创建一个与该套接字相关的文件
4.4.1 TCP编程
4.4.1.1 建立TCP服务器连接的6个步骤
1、创建socket对象。调用socket构造函数:
基本语法: socket.socket(socket_family, socket_type, protocal=0)
参数:
socket_family: socket 地址家族, AF_UNIX/AF_LOCAL 或者 AF_INET
socket_type:socket 连接类型:1)面向连接的(SOCK_STREAM) 2)面向无连接的(SOCK_DGRAME) protocal:
传输协议,一般不用设置,使用默认值进行自动匹配就好
如果IP地址为 0.0.0.0 代表本机的任意一个IP 端口 0--1024 为系统保留
IP地址为localhost代表本地主机,指这台计算机,相对应的ip地址为127.0.0.1
3、绑定后,必须准备好套接字,以便接受连接请求:
4.4.1.2 建立TCP客服端

1 import socket 2 # 定义服务器信息 3 print('初始化服务器主机信息') 4 HOST = socket.gethostname() # 本地主机,指这台计算机,相对应的ip地址为127.0.0.1 5 PORT = 5000 #端口 0--1024 为系统保留 6 ADDRESS = (HOST, PORT) 7 BUFFER = 1024 #数据发送和接受的最大数据大小 8 9 # 创建TCP服务socket对象 10 print("初始化服务器主机套接字对象......") 11 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 12 # 面向网络的套接字:通过网络进行数据交互, TCP协议,server就是socket的实例 13 14 # 绑定主机信息 15 print('绑定的主机信息......') 16 server.bind(ADDRESS) #元组,相当于一个参数 17 18 # 启动服务器 一个只能接受一个客户端请求,可以有10个请求排队 19 print("开始启动服务器......") 20 server.listen(10) 21 22 #等待连接 23 while True: 24 # 等待来自客户端的连接 25 print('等待客户端连接') 26 conn, addr = server.accept() # 等电话 27 print('连接的客服端套接字对象为:{}\n客服端的IP地址(拨进电话号码):{}'.format(conn, addr)) 28 # 发送给客户端的数据 29 #server.send("欢迎访问服务器".encode('utf-8')) 30 31 #收发信息,可以收发多次 32 while True: 33 # 接收客户端发送的数据 34 print("等待客户端发送信息:") 35 msg = conn.recv(BUFFER) 36 try: 37 print("client客户端:{}".format(msg.decode("utf-8")))

1 import socket 2 # 定义要连接的服务器信息 3 HOST = socket.gethostname() 4 PORT = 5000 5 ADDRESS = (HOST, PORT) 6 7 #创建客户端套接字对象 8 client = socket.socket()# 相当于声明socket类型,同时生成socket链接对象,默认值 9 10 #连接服务器 11 client.connect(ADDRESS) 12 print('欢迎连接服务器') 13 # 和服务器进行数据交互 14 while True: 15 # 给服务器发送消息 16 info = input("请输入要发送的信息:") 17 client.send(info.encode("utf-8")) 18 19 if info == 'bye': 20 client.close() 21 print("客户端退出") 22 break 23 #接受服务端信息 24 print("等待服务端发送信息:") 25 data = client.recv(1024) 26 try: 27 print("server服务端端:{}".format(data.decode("utf-8"))) 28 29 except Exception: 30 print("server服务端端:{}".format(data.decode("gbk")))
4.4.2 UDP编程
udp服务端
1 ss = socket() #创建一个服务器的套接字 2 ss.bind() #绑定服务器套接字 3 inf_loop: #服务器无限循环 4 cs = ss.recvfrom()/ss.sendto() # 对话(接收与发送) 5 ss.close()
udp客户端
1 cs = socket() # 创建客户套接字 2 comm_loop: # 通讯循环 3 cs.sendto()/cs.recvfrom() # 对话(发送/接收) 4 cs.close() # 关闭客户套接字

1 from socket import * 2 3 ip_port = ('127.0.0.1', 8080) 4 buffer_size = 1024 5 6 udp_server = socket(AF_INET, SOCK_DGRAM) # 数据报套接字 7 udp_server.bind(ip_port) 8 9 while True: 10 data, addr = udp_server.recvfrom(buffer_size) 11 print(data.decode('utf-8')) 12 print('data from', addr) 13 udp_server.sendto(data.upper(), addr) # upper() 小写变大写

1 from socket import * 2 ip_port = ('127.0.0.1', 8080) #服务端IP+端口 3 buffer_size = 1024 4 5 udp_client = socket(AF_INET, SOCK_DGRAM) #udp数据报套接字 6 7 while True: 8 msg = input('>>:').strip() 9 udp_client.sendto(msg.encode('utf-8'), ip_port) 10 #数据,ip地址+端口 11 data, addr = udp_client.recvfrom(buffer_size) 12 print(data.decode('utf-8')) 13 print('data from %s',addr)