python 全栈开发,Day33(tcp协议和udp协议,互联网协议与osi模型,socket概念,套接字(socket)初使用)

先来回顾一下昨天的内容

网络编程
开发架构
  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是因特网中的传输层协议,使用三次握手协议建立连接。当主动方发出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] 
TCP四次断开

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概念

socket层

理解socket

Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

其实站在你的角度上看,socket就是一个模块。我们通过调用模块中已经实现的方法建立两个进程之间的连接和通信。
也有人将socket说成ip+port,因为ip是用来标识互联网中的一台主机的位置,而port是用来标识这台机器上的一个应用程序。
所以我们只要确立了ip和port就能找到一个应用程序,并且使用socket模块来与之通信。
站在你的角度理解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()        #关闭服务器套接字(可选)
server

比如把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

新建文件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和端口
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协议
    对应设备:网桥,以太网交换机,网卡
物理层
    对象协议:无
    对应设备:中继器,集线器,双绞线

  

  

 

posted @ 2018-05-03 15:14  肖祥  阅读(900)  评论(0编辑  收藏  举报