Python网络编程-一文厘清socket、TCP和UDP那点事

2022-05-08 09:14:23 浏览数 (1)

文章目录

  • 网络基础
    • 网络协议
    • IP地址与端口
    • socket套接字
      • 概念
      • Python中socket模块
  • TCP下的服务器与客户端
    • TCP工作原理
    • TCP服务器的实现
    • TCP客户端的实现
  • UDP下的服务器与客户端
    • UDP工作原理
    • UDP服务器的实现
    • UDP客户端的实现

网络基础

网络协议

网络协议是计算机网络数据进行彼此交换而建立起的规则或标准。就像我们说的普通话一样,网络协议是计算机设备间的“普通话”,是一种彼此交流的方式。更多计算机网络总结可参考这篇博客,此处不便赘述。

不得不提网络协议三要素:语义、语法、同步

  • 语义:即需要发出何种控制信息,完成何种动作以及做出何种相应,“讲什么”。
  • 语法:即数据与控制信息的结构或格式,“怎么讲”。
  • 同步:即事件实现顺序的详细说明。

在著名的OSI/RM模型中,将网络协议划分为7层,如下图所示:

网络协议中最为重要的是TCP/IP协议,它是互联网的基础协议。TCP/IP协议并不是TCP和IP协议的合称,是因特网整个网络TCP/IP协议簇。协议体系结构如图中四个层次,包括网络接口层、网络层、传输层、应用层。

IP地址与端口

IP(Internet Protocol)是计算机网络相互连接进行通信而设计的协议,位于TCP/IP协议簇体系网络层中。它规定了计算机在因特网上进行通信时应当遵守的规则,是所有计算机网络实现通信的一套规则。换句话说,任何计算机系统只要遵守IP协议就可以与因特网互联互通。

每一台主机都有一个唯一的IP地址,IP协议正是利用IP地址在主机间传递信息。IP地址由网络标识号码与主机标识号码两部分组成,可以分为ABCDE五类,分别适用大型网络、中型网络、小型网络、多目地址、备用。可以在cmd输入ipconfig查看信息。

IP地址不便于记忆,通常会使用主机名来代替IP地址,即使用DNS域名解析协议。比如输入“baidu.com”就能访问到百度了,不必输百度的IP地址。

端口是计算机与外界进行通信交流的出口,我们通过IP或域名访问到一台具体的计算机后,可以通过端口号来访问这台计算机上对应的软件或服务。常用的保留TCP端口号有HTTP80、FTP20/21、Telnet23、SMTP25、DNS53等。

socket套接字

概念

socket是网络通信端口的一种现象,也称套接字。用于描述IP地址和端口,是一个通信链的句柄,以实现不同计算机间的通信,可以比喻成一个多孔插座,不同型号的插座得到不同的服务。具体来说就是两个程序通过一个双向通信连接实现数据的交换,而这个连接的一段就是一个socket。

socket是应用层与TCP/IP协议簇通信的中间软件抽象层,是应用层与运输层间的桥梁,如下图所示:

Python中socket模块

一、socket模块中的socket类 Python中,可以通过socket模块实现网络通信,该模块提供了一个scoket类,定义如下:

代码语言:javascript复制
class socket(_socket.socket):
	def __init__(self,family=AF_INET,type=SOCK_STREAM,proto=0)

从上述定义看出,socket类是_socket.socket子类,根据给定的地址簇、套接字类型和协议号创建一个新的socket。套接字是通过地址簇套接字类型两个主要属性来控制如何发送数据。如下:

  • family套接字地址簇 可取值有AF_INET(默认,用于IPv4寻址)、AF_INIET6(用于IPv6寻址)、AF_UNIX(UNIX域套接字的地址簇,仅支持UDS系统)等等。一般默认值是最高效的。
  • type套接字类型 默认SOCK_STREAM,还可取SOCK_DGRAMSOCK_RAW等。SOCK_STREAM对应传输控制协议TCP。 TCP确保每条信息按顺序正确发送,而UDP传送没有顺序,可能多次传送或不传送,适合广播。
  • proto协议编号 通常为0,可以忽略

由socket类创建的socket对象有一系列方法及属性,篇幅限制(偷懒 )不再一一演示,梳理如下(建议收藏):

名称

描述

服务器套接字方法

sock.bind()

将地址绑定到套接字上

sock.listen()

设置并启动TCP监听器

sock.accept()

被动接收TCP客户端连接,一直阻塞直到连接到达

客户端套接字方法

sock.connect()

发起TCP客户端连接

sock.connect_ex()

connect()扩展版本,会以错误码形式显示异常

普通的套接字方法

sock.recv()

接收TCP消息

sock.recv_into()

接收TCP消息到指定缓冲区

sock.send()

发送TCP消息

sock.sendall()

完整发送TCP消息

sock.recvfrom()

接收UDP消息

sock.recvfrom_into()

接收UDP消息到指定的缓冲区

sock.sendto()

发送UDP消息

sock.getpeername()

连接到套接字的远程地址

sock.getsockname()

获取当前套接字地址

sock.getsockopt()

获取给定套接字选项的值

sock.shutdown()

关闭连接

sock.share()

复制套接字并准备与目标进程共享

sock.close()

关闭套接字

sock.detach()

在未关闭文件描述符的情况下关闭套接字并返回文件描述符

sock.ioctl()

控制套接字的模式

面向阻塞的套接字方法

sock.setblocking()

设置套接字的阻塞或非阻塞模式

sock.gettimeout()

获取阻塞套接字操作的超时时间

面向文件的套接字方法

sock.fileno()

套接字的文件描述符

sock.makefile()

创建与套接字关联的文件对象

数据属性

sock.family()

套接字家族

sock.type()

套接字类型

sock.proto()

套接字协议

二、socket模块中其他功能函数

小结如下(建议码住):

名称

描述

属性

AF_UNIX、AF_INET、AF_INET6、AF_NETLINK、AF_TIPC

Python中支持的套接字地址家族

SO_STREAM、SO_DGRAM

套接字类型

has_ipv6

是否支持IPv6

异常

error

套接字相关错误

herror

主机和地址相关错误

gaierror

地址相关错误

timeout

超时时间

方法

socket()

以给定的地址家族、套接字类型和协议类型创业一个套接字对象

socketpair()

以给定的地址家族、套接字类型和协议类型创业一对套接字对象

create_connection()

接收一个地址,返回套接字对象

fromfd()

以一个打开的文件描述符创建一个套接字对象

ssl()

通过套接字启动一个安全套接字连接,不执行证书验证

getaddrinfo()

获取一个五元组序列形式的地址信息

getnameinfo()

以给定的套接字地址,返回二元组(主机名,端口号)

getfqdn()

返回完整的域名

gethostname()

返回当前主机名

gethostbyname()

将一个主机名映射到它的IP地址

gethostbyname_ex()

gethostbyname()扩展版本,返回主机名、别名主机集合和IP地址列表

gethostbyaddr()

将一个IP地址映射到DNS信息,返回与gethostbyname_ex()相同的三元组

getprotobyname()

将协议名映射到一个数字

getservbyname()

将服务名映射到一个协议名

getservbyport()

将服务名映射到一个端口号

ntohl()/ntohs()

将来自网络的整数转换为主机字节顺序

htonl()/htons()

将来自网络的整数转换为网络字节顺序

inet_aton()/inet_ntoa()

将IP地址八进制字符串转换为32位的包格式,或者反过来

getdefaulttimeout()

返回默认套接字超时时间

setdefaulttimeout()

设置默认套接字超时时间

几个常用举例:

代码语言:javascript复制
import socket
print('-----将主机端口转换为五元组(family,type,proto,canonname,sockaddr):')
print(socket.getaddrinfo('baidu.com',port=80))
print(socket.getaddrinfo('example.org',80,proto=socket.IPPROTO_TCP))
print('-----当前主机名')
print(socket.gethostname())
print('-----返回限制域名:')
print(socket.getfqdn())  #不带参默认本机
print(socket.getfqdn('baidu.com'))  #可能网络影响有点慢
print(socket.getfqdn('123.110.50.216'))
print('-----主机名转换IP地址')
print(socket.gethostbyname('baidu.com'))
print(socket.gethostbyname('123.125.105.110'))
print('-----主机名转换IP地址并返回三元组(hostname,aliaslist,ipaddrlist)')
print(socket.gethostbyname_ex('baidu.com'))

TCP下的服务器与客户端

TCP(Transmission Control Protocol)传输控制协议是一种面向连接的、可靠的和基于字节流的传输层通信协议,用于在不可靠的互联网络上提供可靠的、端对端字节流传输服务。

位于TCP/IP体系结构的传输层是处在IP层之上、应用层之下的中间层,所以数据传输必须经过IP层。当应用层想TCP层发送用于网间传输、用八位字节表示的数据流时,TCP把数据流分割成适当长度的报文段,然后把离散的报文组装为比特流。为了保障数据的可靠传输,会对从应用层传送到TCP实体的数据进行监管,并提供了重发机制和流控制。

TCP工作原理

TCP是如何保障数据可靠不丢失且有序呢?答案是对传输数据按字节进行了编号,编号的目的是保证传送到接收端的数据能够按顺序接收。接收端会对已经接收的数据发回一个确认,若发送端在规定时间内未收到有编号的数据,则将重新传送前面的数据。

如何编号?TCP不是使用顺序的整数作为数据包的编号,而是通过一个计数器记录发送的字节数,且TCP初始序列号是随机选择的,这样可以避免TCP序号易于猜测而伪造数据进行欺骗或攻击。比如包大小是2048字节,初始序号为3000,那么下一个数据包的序号是5048。

此外,TCP可以一次性发送多个数据包,无须按数据包依次发送。同时可以通过发送方传输的数据量大小来进行减缓或暂停(流量控制),若发送数据包丢弃,就会减少每秒发送的数据量。

结合前面讲的socket模块,要如何进行TCP通信呢?先从服务器开始:初始化Socket、然后绑定(bind)端口、监听(listen)端口、调用accept阻塞、最后等待客户端连接;某个客户端初始化一个Socket,然后连接(connect)服务器。若连接成功,那么客户端与服务器的连接就建立了,客户端发送数据请求,服务器接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互就结束了。上图解:

TCP服务器的实现

创建TcpServer.py,使用socket模块实现TCP服务器,启动服务器,等待客户端连接,详见注释:

代码语言:javascript复制
import socket

HOST = 'localhost'  # 主机
PORT = 6666   # 端口
BUF_SIZE = 1024  # 最大字节数
ADDRESS = (HOST, PORT)  # 地址(IP,端口)

if __name__ == '__main__':
    # 新建socket连接
    server_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    # 将套接字与指定IP端口连接
    server_socket.bind(ADDRESS)
    # 启动监听(并设最大连接数为5)
    server_socket.listen(5)
    print('正在监听:%s:%d' % (HOST, PORT))
    while True:
        print(u'服务器已就绪,等待连接中...')
        # 当有连接时,把收到的套接字存到client_sock,远程连接细节存到address中
        client_sock, address = server_socket.accept()
        print(u'连接客户端地址:',address)
        while True:
            # 接收数据
            data = client_sock.recv(BUF_SIZE)
            if not data or data == 0:
                break
            print('来自客户端信息:%s' % data.decode('utf-8'))
            # 发送数据
            client_sock.send('好的'.encode('utf-8'))
        client_sock.close()  # 关闭客户端
    server_socket.close()  # 关闭socket

运行服务器:

TCP客户端的实现

新建TcpClient.py:

代码语言:javascript复制
import socket

HOST = 'localhost'
PORT = 6666  # 注意端口一致
BUF_SIZE = 1024
ADDRESS = (HOST, PORT)

if __name__ == '__main__':
    # 创建socket
    sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    # 连接服务器
    sock.connect(ADDRESS)
    print('成功连接目标主机:%s,目标主机端口:%s' % (HOST,PORT))
    # 发送数据
    sock.send('记得一键三连~'.encode('utf-8'))
    # 接收数据
    msg=sock.recv(BUF_SIZE)
    print('来自服务器信息:%s' % msg.decode('utf-8'))
    #关闭连接
    sock.close()

运行客户端:

服务器结果:

注意发送接收数据时以bytes进行而不是string,要不然会报错“TypeError: a bytes-like object is required, not ‘str’”,所以使用encode()decode()进行编码和解码。

UDP下的服务器与客户端

UDP(User Datagram Protocol)用户数据报协议是OSI模型中一种无连接的传输层协议,提供了面向事务的简单不可靠消息传送服务。

UDP同TCP一样也是用于处理数据包,不过它只负责将应用层的数据发送出去,不具备差错控制和流量控制。因此在传送过程中如果数据出错就要由高层协议处理,但也因为没有差错控制和流量控制的开销,所以使得传输效率高、延时小,适用于对可靠性要求不高的应用,可以快速大量的发送数据但不负责可靠性,同文章开头表情包,快不快就完事了

UDP工作原理

UDP提供不可靠的无连接数据包传输服务,使用底层互联网协议传送报文,IP报文协议号是17,其报文是封装在IP数据报中进行传输的。UDP报文由UDP源端口自动、UDP目标端口字段、UDP报文长度字段、UDP校验和字段以及数据区组成。首先通过端口机制进行复用和分解,每个UDP应用程序在发送数据报文之前,必须与操作系统协商获取相应的协议端口及端口号,然后根据目的端口号进行分解,接收端使用UDP的校验进行确认,查看UDP报文是否正确到达了目标主机的相应端口。

UDP服务器的实现

新建UdpServer.py:

代码语言:javascript复制
import socket
BUF_SIZE = 1024

if __name__ == '__main__':
    # 新建socket连接(用SOCK_DGRAM即UDP=数据报)
    sock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
    # 绑定主机和端口
    sock.bind(('localhost',8888))
    while True:
        print(u'服务器已就绪,等待连接中...')
        # 当也有连接时,将接收数据存到data,远程连接细节存到address
        data, address = sock.recvfrom(BUF_SIZE)
        print('连接客户端地址:', address)
        print('来自客户端信息:%s' % data.decode('utf-8'))
        # 发送数据
        sock.sendto('收到'.encode('utf-8'),address)

运行服务器:

UDP客户端的实现

新建UdpClient.py:

代码语言:javascript复制
import socket
BUF_SIZE = 1024

if __name__ == '__main__':
    # 新建socket连接
    sock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
    # 发送数据
    sock.sendto('在线蹲个一键三连~'.encode(), ('localhost', 8888))
    # 接收数据
    data, address = sock.recvfrom(BUF_SIZE)
    print('连接服务器地址:', address)
    print('来自服务器信息:%s' % data.decode())

运行客户端:

服务器结果:

注意UDP与TCP连接不同的是socket.socket()的第二个参数。TCP使用SOCK_STREAM,UDP使用SOCK_DGRAM

0 人点赞