先来回顾一下昨天的内容
网络编程
开发架构
B/S C/S 架构
网卡
mac 地址
网段
ip 地址 : 表示了一台电脑在网络中的位置
子网掩码 : ip 和子网掩码按位与得到网段
网关 ip : 内置在路由器中的
交换机 :能够保证在一个局域网内的机器之间通信
路由器 :跨局域网之间的通信 - 路由表

arp 协议 ——查询IP地址和MAC地址的对应关系
地址解析协议,即 ARP(Address Resolution Protocol),是根据 IP 地址获取物理地址的一个 TCP/IP 协议。
主机发送信息时将包含目标 IP 地址的 ARP 请求广播到网络上的所有主机,并接收返回消息,以此确定目标的物理地址。
收到返回消息后将该 IP 地址和物理地址存入本机 ARP 缓存中并保留一定时间,下次请求时直接查询 ARP 缓存以节约资源。
为什么要有局域网?
因为 IP 地址不够用
全国的网络地址范围为 0.0.0.0 - 255.255.255.255
其中有几个保留 IP 地址,是给内网使用的。
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
透过局域网 我们之所以可以访问外网 是因为路由器中有一个网关 ip,
这个 ip 是整个局域网中所有机器与外界通讯的媒介,
不能让我们的机器对外提供服务
外网 ip 我们在任何地方都能访问到的地址就是外网地址
全世界唯一
192.168.. 是不能对外提供服务的
第一 你的 ip 是一个局域网地址
第二 你没有一个外网的 ip,网关 ip
交换机只能看懂 mac 地址,Ip 地址是看不懂的
路由器是能看懂 ip 地址的。
一、tcp 协议和 udp 协议
用于应用程序之间的通信。如果说 ip 地址和 mac 地址帮我们确定唯一的一台机器,那么我们怎么找到一台机器上的一个软件呢?
端口
我们知道,一台拥有 IP 地址的主机可以提供许多服务,比如 Web 服务、FTP 服务、SMTP 服务等,这些服务完全可以通过 1 个 IP 地址来实现。那么,主机是怎样区分不同的网络服务呢?显然不能只靠 IP 地址,因为 IP 地址与网络服务的关系是一对多的关系。实际上是通过“IP 地址+端口号”来区分不同的服务的。
端口的概念 是虚拟的
端口的范围是 0 - 65535
在同一台机器上 同一时刻 每一个端口只能为一个运行中的程序提供服务
只有用到联网通信的程序才会用到端口的概念
ip + 端口 就可以找到全世界唯一的一台电脑上的一个程序,比如 112.34.112.40:80
网络访问必须经过的一条路 —— 端口

qq 是基于 udp,加强了功能,保证消息发送成功
qq 发送消息,不能发太长
TCP 协议
当应用程序希望通过 TCP 与另一个应用程序通信时,它会发送一个通信请求。这个请求必须被送到一个确切的地址。在双方“握手”之后,TCP 将在两个应用程序之间建立一个全双工 (full-duplex) 的通信。
这个全双工的通信将占用两个计算机之间的通信线路,直到它被一方或双方关闭为止。
先来一个普通版的
三次握手

四次挥手

四次挥手,也称之为 四次断开
下面看官方的图

为什么 TCP 连接需要 3 次,而断开需要四次呢?
在 Client 发送出最后的 ACK 回复,但该 ACK 可能丢失。Server 如果没有收到 ACK,将不断重复发送 FIN 片段。所以 Client 不能立即关闭,它必须确认 Server 接收到了该 ACK。Client 会在发送出 ACK 之后进入到 TIME_WAIT 状态。Client 会设置一个计时器,等待 2MSL 的时间。如果在该时间内再次收到 FIN,那么 Client 会重发 ACK 并再次等待 2MSL。所谓的 2MSL 是两倍的 MSL(Maximum Segment Lifetime)。MSL 指一个片段在网络中最大的存活时间,2MSL 就是一个发送和一个回复所需的最大时间。如果直到 2MSL,Client 都没有再次收到 FIN,那么 Client 推断 ACK 已经被成功接收,则结束 TCP 连接。
这个网上转载的例子不错:
三次握手:
A:“喂,你听得到吗?”A->SYN_SEND
B:“我听得到呀,你听得到我吗?”应答与请求同时发出 B->SYN_RCVD | A->ESTABLISHED
A:“我能听到你,今天 balabala……”B->ESTABLISHED
四次挥手:
A:“喂,我不说了。”A->FIN_WAIT1
B:“我知道了。等下,上一句还没说完。Balabala…..”B->CLOSE_WAIT | A->FIN_WAIT2
B:”好了,说完了,我也不说了。”B->LAST_ACK
A:”我知道了。”A->TIME_WAIT | B->CLOSED
A 等待 2MSL,保证 B 收到了消息,否则重说一次”我知道了”,A->CLOSED
TCP三次握手
TCP是因特网中的传输层协议,使用三次握手协议建立连接。当主动方发出SYN连接请求后,等待对方回答SYN+ACK[1],并最终对对方的 SYN 执行 ACK 确认。这种建立连接的方法可以防止产生错误的连接。[1]
TCP三次握手的过程如下:
客户端发送SYN(SEQ=x)报文给服务器端,进入SYN_SEND状态。
服务器端收到SYN报文,回应一个SYN (SEQ=y)ACK(ACK=x+1)报文,进入SYN_RECV状态。
客户端收到服务器端的SYN报文,回应一个ACK(ACK=y+1)报文,进入Established状态。
三次握手完成,TCP客户端和服务器端成功地建立连接,可以开始传输数据了。
TCP四次断开
建立一个连接需要三次握手,而终止一个连接要经过四次握手,这是由TCP的半关闭(half-close)造成的。
(1) 某个应用进程首先调用close,称该端执行“主动关闭”(active close)。该端的TCP于是发送一个FIN分节,表示数据发送完毕。
(2) 接收到这个FIN的对端执行 “被动关闭”(passive close),这个FIN由TCP确认。
注意:FIN的接收也作为一个文件结束符(end-of-file)传递给接收端应用进程,放在已排队等候该应用进程接收的任何其他数据之后,因为,FIN的接收意味着接收端应用进程在相应连接上再无额外数据可接收。
(3) 一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个FIN。
(4) 接收这个最终FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN。[1]
既然每个方向都需要一个FIN和一个ACK,因此通常需要4个分节。
注意:
(1) “通常”是指,某些情况下,步骤1的FIN随数据一起发送,另外,步骤2和步骤3发送的分节都出自执行被动关闭那一端,有可能被合并成一个分节。[2]
(2) 在步骤2与步骤3之间,从执行被动关闭一端到执行主动关闭一端流动数据是可能的,这称为“半关闭”(half-close)。
(3) 当一个Unix进程无论自愿地(调用exit或从main函数返回)还是非自愿地(收到一个终止本进程的信号)终止时,所有打开的描述符都被关闭,这也导致仍然打开的任何TCP连接上也发出一个FIN。
无论是客户还是服务器,任何一端都可以执行主动关闭。通常情况是,客户执行主动关闭,但是某些协议,例如,HTTP/1.0却由服务器执行主动关闭。[2]
UDP 协议
当应用程序希望通过 UDP 与一个应用程序通信时,传输数据之前源端和终端不建立连接。
当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。
tcp 和 udp 的对比
TCP—-传输控制协议,提供的是面向连接、可靠的字节流服务。当客户和服务器彼此交换数据前,必须先在双方之间建立一个 TCP 连接,之后才能传输数据。TCP 提供超时重发,丢弃重复数据,检验数据,流量控制等功能,保证数据能从一端传到另一端。
UDP—-用户数据报协议,是一个简单的面向数据报的运输层协议。UDP 不提供可靠性,它只是把应用程序传给 IP 层的数据报发送出去,但是并不能保证它们能到达目的地。由于 UDP 在传输数据报前不用在客户和服务器之间建立一个连接,且没有超时重发等机制,故而传输速度很快
更多
现在Internet上流行的协议是TCP/IP协议,该协议中对低于1024的端口都有确切的定义,他们对应着Internet上一些常见的服务。这些常见的服务可以分为使用TCP端口(面向连接)和使用UDP端口(面向无连接)两种。
说到TCP和UDP,首先要明白“连接”和“无连接”的含义,他们的关系可以用一个形象地比喻来说明,就是打电话和写信。两个人如果要通话,首先要建立连接——即打电话时的拨号,等待响应后——即接听电话后,才能相互传递信息,最后还要断开连接——即挂电话。写信就比较简单了,填写好收信人的地址后将信投入邮筒,收信人就可以收到了。从这个分析可以看出,建立连接可以在需要痛心地双方建立一个传递信息的通道,在发送方发送请求连接信息接收方响应后,由于是在接受方响应后才开始传递信息,而且是在一个通道中传送,因此接受方能比较完整地收到发送方发出的信息,即信息传递的可靠性比较高。但也正因为需要建立连接,使资源开销加大(在建立连接前必须等待接受方响应,传输信息过程中必须确认信息是否传到及断开连接时发出相应的信号等),独占一个通道,在断开连接钱不能建立另一个连接,即两人在通话过程中第三方不能打入电话。而无连接是一开始就发送信息(严格说来,这是没有开始、结束的),只是一次性的传递,是先不需要接受方的响应,因而在一定程度上也无法保证信息传递的可靠性了,就像写信一样,我们只是将信寄出去,却不能保证收信人一定可以收到。
TCP是面向连接的,有比较高的可靠性, 一些要求比较高的服务一般使用这个协议,如FTP、Telnet、SMTP、HTTP、POP3等。
而UDP是面向无连接的,使用这个协议的常见服务有DNS、SNMP、QQ等。对于QQ必须另外说明一下,QQ2003以前是只使用UDP协议的,其服务器使用8000端口,侦听是否有信息传来,客户端使用4000端口,向外发送信息(这也就不难理解在一般的显IP的QQ版本中显示好友的IP地址信息中端口常为4000或其后续端口的原因了),即QQ程序既接受服务又提供服务,在以后的QQ版本中也支持使用TCP协议了。
二、互联网协议与 osi 模型
互联网协议按照功能不同分为 osi 七层或 tcp/ip 五层或 tcp/ip 四层

每层运行常见物理设备

每层运行常见的协议

相关解释

数据发送是从上向下进行的
数据都是二进制的

上层设备,能解析下层的数据,比如三层交换机能解析 MAC

三、socket 概念
1.socket 层

2.理解 socket
Socket 是应用层与 TCP/IP 协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket 其实就是一个门面模式,它把复杂的 TCP/IP 协议族隐藏在 Socket 接口后面,对用户来说,一组简单的接口就是全部,让 Socket 去组织数据,以符合指定的协议。
其实站在你的角度上看,socket就是一个模块。我们通过调用模块中已经实现的方法建立两个进程之间的连接和通信。
也有人将socket说成ip+port,因为ip是用来标识互联网中的一台主机的位置,而port是用来标识这台机器上的一个应用程序。
所以我们只要确立了ip和port就能找到一个应用程序,并且使用socket模块来与之通信。
socket 仍然是底层,写代码做网络通信的基础,都是基于 socket 的
socket 隐藏了上层(应用层)和下层(传输层,网络层,链路层),基于这 2 层之间,做数据交互。

3.套接字(socket)的发展史
套接字起源于 20 世纪 70 年代加利福尼亚大学伯克利分校版本的 Unix,即人们所说的 BSD Unix。 因此,有时人们也把套接字称为“伯克利套接字”或“BSD 套接字”。一开始,套接字被设计用在同 一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或 IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。
基于文件类型的套接字家族
套接字家族的名字:AF_UNIX
unix 一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信
基于网络类型的套接字家族
套接字家族的名字:AF_INET
(还有 AF_INET6 被用于 ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET 是使用最广泛的一个,python 支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用 AF_INET)
现在主流是 AF_INET
4.tcp 协议和 udp 协议
TCP(Transmission Control Protocol)可靠的、面向连接的协议(eg:打电话)、传输效率低全双工通信(发送缓存&接收缓存)、面向字节流。使用 TCP 的应用:Web 浏览器;电子邮件、文件传输程序。
UDP(User Datagram Protocol)不可靠的、无连接的服务,传输效率高(发送前时延小),一对一、一对多、多对一、多对多、面向报文,尽最大努力服务,无拥塞控制。使用 UDP 的应用:域名系统 (DNS);视频流;IP 语音(VoIP)。
我知道说这些你们也不懂,直接上图。

四.套接字(socket)初使用
基于 TCP 协议的 socket
tcp 是基于链接的,必须先启动服务端,然后再启动客户端去链接服务端
server
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',8898)) #把地址绑定到套接字
sk.listen() #监听链接
conn,addr = sk.accept() #接受客户端链接
ret = conn.recv(1024) #接收客户端信息
print(ret) #打印客户端信息
conn.send(b'hi') #向客户端发送信息
conn.close() #关闭客户端套接字
sk.close() #关闭服务器套接字(可选)
比如把 socket 比喻成打电话
新建文件 server.py,内容如下:
import socket
# 基于tcp协议的一次通信
sk = socket.socket() # 买手机
# sk.bind(('192.168.11.53',9999)) # 装一张电话卡
sk.bind(('127.0.0.1',9999)) # 装一张电话卡
# 8000 - 10000
sk.listen() # 开机
conn,addr = sk.accept() # 等着 接电话 我们两个的连接,对方的地址
#print(addr)
conn.send('你好'.encode('utf-8'))
ret = conn.recv(1024) #1024 表示接受1024个字节
print(ret.decode('utf-8'))
conn.close() # 挂电话
sk.close() # 关手机
client
import socket
sk = socket.socket() # 创建客户套接字
sk.connect(('127.0.0.1',8898)) # 尝试连接服务器
sk.send(b'hello!')
ret = sk.recv(1024) # 对话(发送/接收)
print(ret)
sk.close() # 关闭客户套接字
新建文件 client.py,内容如下:
import socket
sk = socket.socket() # 买个手机
sk.connect(('127.0.0.1',9999)) # 打电话
ret = sk.recv(1024)
print(ret.decode('utf-8'))
sk.send('你也好'.encode('utf-8'))
sk.close() # 关机
问题:有的同学在重启服务端时可能会遇到

解决方法:
加入一条socket配置,重用ip和端口
#加入一条socket配置,重用ip和端口
import socket
from socket import SOL_SOCKET,SO_REUSEADDR
sk = socket.socket()
sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加
sk.bind(('127.0.0.1',8898)) #把地址绑定到套接字
sk.listen() #监听链接
conn,addr = sk.accept() #接受客户端链接
ret = conn.recv(1024) #接收客户端信息
print(ret) #打印客户端信息
conn.send(b'hi') #向客户端发送信息
conn.close() #关闭客户端套接字
sk.close() #关闭服务器套接字(可选)
由于是本机测试,所以 server 和 client 的 IP 地址为 127.0.0.1
如果是不通的电脑通信,需要修改成真实 IP,比如 192.168.11.34
先执行 server.py,执行输出,没有结果的。因为它正在监听客户端发送数据
再执行 client.py,执行输出:你好
server.py 这边输出了:你也好
2 个 py 文件,结束进程。
tcp 比喻成打电话
udp 比喻成发短信,因为它不是即时通讯
server.py 参数解释:
sk.bing() 里面的数据,必须是一个元组。分别是 IP 地址和端口
IP 地址可以写成本机回环地址 127.0.0.1,它只能在本机通信。不需要通过交换机。如果需要网络通信,要写成真实的 IP 地址。
从端口的分配来看,端口被分为固定端口和动态端口两大类(一些教程还将极少被用到的高端口划分为第三类:私有端口):
固定端口(0-1023):使用集中式管理机制,即服从一个管理机构对端口的指派,这个机构负责发布这些指派。比如 HTTP 用的是 80 端口
动态端口(1024-49151):这些端口并不被固定捆绑某一服务,操作系统将这些端口动态的分配给各个进程, 同一进程两次分配有可能分配到不同的端口。
所以写程序的时候,开端口一般是 8000~10000 之间的端口,基本上是比较安全的,一般不会占用。
conn.send() 参数必须是 bytes 类型的
conn.recv() 参数必须是数字类型,表示最大接收多少字节
socket.socker() 默认是 tcp
查看源码
def __init__(self, family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None):
# For user code address family and type values are IntEnum members, but
# for the underlying _socket.socket they're just integers. The
# constructor of _socket.socket converts the given argument to an
# integer automatically.
_socket.socket.__init__(self, family, type, proto, fileno)
self._io_refs = 0
self._closed = False
type 类型为 SOCK_STREAM,表示 TCP (默认)
sk.accecp() 执行这句,表示建立了 3 次握手
conn.close() 执行这句,表示经历了 4 次挥手
sk.close() 关闭套接字,不在接收客户端请求。
既然服务端是 TCP,那么客户端,也一定是 TCP
客户端断开,直接 sk.close()即可。
为啥 server 要 close2 次呢?
因为 server 连接是基于 conn 的,server 可以接收很多请求。
一个 conn 处理一个请求。
server 的发送和接收,使用 conn。所以数据交互完毕,需要关闭连接。
而 client 是单向的,统一使用 sk.colse()就可以关闭连接了。
思考题,能多发几次消息吗?像 qq 一样。
将 send 变成 input,默认相互发送
客户端输入 q 时,先断开服务器连接和程序,再断开客户端程序。
server.py 代码如下:
import socket
while True:
sk = socket.socket() # 创建套接字
sk.bind(('127.0.0.1', 9999)) # 把地址绑定到套接字
sk.listen() # 监听连接
conn, addr = sk.accept() # 等待接受客户端连接
print(addr) # 打印客户端IP和端口号
ret = conn.recv(1024) # 最大接收1024字节
msg = ret.decode('utf-8') # 接收的信息解码
print(msg) # 打印接收信息
if msg.upper() == 'Q': # 判断接收的信息是否为q
conn.close() # 关闭客户端套接字
sk.close() # 关闭服务器套接字,不再接收请求
break # 退出while循环
content = input('>>>').strip()
conn.send(content.encode('utf-8')) # 给客户端发送消息
clientt.py 代码如下:
import socket
while True:
sk = socket.socket() # 创建客户套接字
sk.connect(('127.0.0.1', 9999)) # 尝试连接服务器
content = input('>>>').strip()
sk.send(content.encode('utf-8')) # 发送数据
ret = sk.recv(1024) # 最大接收1024字节
msg = ret.decode('utf-8') # 接收的信息解码
print(msg) # 打印接收信息
if content.upper() == 'Q': # 判断接收的信息是否为q
sk.send(content.encode('utf-8')) # 发送q给服务器
sk.close() # 关闭客户套接字
break # 跳出while循环
先执行 server.py,再执行 client.py
运行效果如下:

将上面的代码改造成面向对象的
并模拟男女对话
对话内容,颜色随机显示,并显示时间
输入 q 时,结束对话,关闭程序。
server.py 代码如下:
import socket
import time
from Prompt import Prompt
class Server(object):
def __init__(self):
self.ip = '127.0.0.1'
self.port = 9999
self.max = 1024
def main(self):
sk = socket.socket() # 创建套接字
sk.bind((self.ip, self.port)) # 把地址绑定到套接字
sk.listen() # 监听连接
conn, addr = sk.accept() # 等待接受客户端连接
# print(addr) # 打印客户端IP和端口号
while True:
ret = conn.recv(self.max) # 最大接收1024字节
msg = ret.decode('utf-8') # 接收的信息解码
# print(msg) # 打印接收信息
print(time.strftime('%Y-%m-%d %H:%M:%S'))
print(Prompt.interlacing_color('男:' + msg))
if msg.upper() == 'Q': # 判断接收的信息是否为q
conn.close() # 关闭客户端套接字
sk.close() # 关闭服务器套接字,不再接收请求
break # 退出while循环
content = input('>>>').strip()
conn.send(repr(content).encode('utf-8')) # 给客户端发送消息
if __name__ == '__main__':
Server().main()
client.py 内容如下:
import socket
import time
from Prompt import Prompt
class Client(object):
def __init__(self):
self.ip = '127.0.0.1'
self.port = 9999
self.max = 1024
def main(self):
sk = socket.socket() # 创建客户套接字
sk.connect((self.ip, self.port)) # 尝试连接服务器
while True:
content = input('>>>').strip()
sk.send(repr(content).encode('utf-8')) # 发送数据
ret = sk.recv(self.max) # 最大接收1024字节
msg = ret.decode('utf-8') # 接收的信息解码
# print(msg) # 打印接收信息
print(time.strftime('%Y-%m-%d %H:%M:%S'))
print(Prompt.interlacing_color('女:' + msg))
if content.upper() == 'Q': # 判断接收的信息是否为q
sk.send(repr(content).encode('utf-8')) # 发送q给服务器
sk.close() # 关闭客户套接字
break # 跳出while循环
if __name__ == '__main__':
Client().main()
Prompt.py 内容如下:
import random
class Prompt(object): # 提示信息显示
colour_dic = {
'red': 31,
'green': 32,
'yellow': 33,
'blue': 34,
'purple_red': 35,
'bluish_blue': 36,
'white': 37,
}
def __init__(self):
pass
@staticmethod
def display(msg, colour='white'):
choice = Prompt.colour_dic.get(colour)
# print(choice)
if choice:
info = "\033[1;{};1m{}\033[0m".format(choice, msg)
return info
else:
return False
def interlacing_color(msg): # 随机换色
colour_list = []
for i in Prompt.colour_dic:
colour_list.append(i)
length = len(colour_list) - 1 # 最大索引值
index = random.randint(0, length) # 随机数
ret = Prompt.display(msg, colour_list[index]) # 随机颜色
return ret
if __name__ == '__main__':
# pass
ret = Prompt.interlacing_color('cccffff')
print(ret)
服务端与客户端不能直接发送列表,元组,字典。需要字符串化 repr(data)
先执行 server.py,再执行 client.py,效果如下:

明日默写:
应用层
对应协议:HTTP,SMTP,POP3
对应设备:无
传输层
对应协议:TCP与UDP协议
对应设备:四层交换机,四层的路由器
网络层
对应协议:IP协议
对应设备:路由器,三层交换机
数据链路层
对应协议:arp协议
对应设备:网桥,以太网交换机,网卡
物理层
对象协议:无
对应设备:中继器,集线器,双绞线