Python9-基于socket的网络编程
1.socket概述
套接字(Socket)是计算机网络编程中的一种抽象概念,用于在网络中传输数据。它提供了一种通信机制,使得不同计算机上的进程(或线程)能够通过网络进行相互通信和数据交换。
套接字可以看作是网络通信的端点,类似于两个进程之间的通信通道。每个套接字都与一个特定的IP地址和端口号相关联,它们用于标识网络中的进程和服务。
套接字可以分为两种类型:流套接字(Stream Socket)和数据报套接字(Datagram Socket)。
- 流套接字(Stream Socket):也称为面向连接的套接字,它提供了一种可靠的、面向连接的数据传输方式。使用流套接字时,数据可以按照顺序传输,确保传输的可靠性和完整性。这种套接字通常基于传输控制协议(TCP)实现,如Web浏览器和服务器之间的通信。
- 数据报套接字(Datagram Socket):也称为无连接套接字,它提供了一种不可靠的、无连接的数据传输方式。通过数据报套接字发送的数据以数据包(Datagram)的形式进行传输,不保证数据的顺序和可靠性。这种套接字通常基于用户数据报协议(UDP)实现,如视频流传输和实时游戏中的数据传输。
总结起来,套接字是计算机网络编程中用于实现进程间通信的一种机制,它提供了不同计算机上进程之间的数据传输通道。通过套接字,可以使用流套接字或数据报套接字进行可靠或不可靠的数据传输。
2.相关api说明
Python中的socket
模块提供了用于网络编程的函数和类。
2.1创建socket对象
socket()
:创建一个套接字对象,语法如下:
socket.socket(family, type, proto=0, fileno=None)
family
:指定套接字地址族,常用的是socket.AF_INET
(IPv4)和socket.AF_INET6
(IPv6)。type
:指定套接字类型,常用的是socket.SOCK_STREAM
(TCP)和socket.SOCK_DGRAM
(UDP)。proto
:可选参数,指定套接字的协议。默认为0,根据地址族和类型自动选择协议。fileno
:可选参数,指定现有文件描述符。通常不需要使用。
2.2主机名和ip地址
socket模块提供包含下列若干函数,用于获取主机名和ip地址等信息。
# 1.获取本地主机名。该函数返回一个字符串,表示本地主机的主机名。
socket.gethostname()
import socket
hostname = socket.gethostname()
print("本地主机名:", hostname) # 本地主机名: shlyyy
# 2.通过主机名获取IP地址。hostname:要获取其IP地址的主机名。
# 该函数接受一个主机名作为参数,并返回对应的IPv4地址的字符串形式。
socket.gethostbyname(hostname)
import socket
hostname = "www.example.com"
ip_address = socket.gethostbyname(hostname)
print("主机名:", hostname) # 主机名: www.example.com
print("IPv4地址:", ip_address) # IPv4地址: 93.184.216.34
该函数只适用于IPv4地址,对于IPv6地址,请使用socket.getaddrinfo()
函数。
# 3.通过主机名获取IP地址及相关信息。hostname:要获取其IP地址的主机名
# 该函数与gethostbyname()类似,不同之处在于它返回一个包含主机名、别名列表和IP地址列表的三元组。
socket.gethostbyname_ex(hostname)
import socket
hostname = "www.example.com"
host_info = socket.gethostbyname_ex(hostname)
print(host_info) # ('www.example.com', [], ['93.184.216.34'])
print("主机名:", host_info[0]) # 主机名: www.example.com
print("别名列表:", host_info[1]) # 别名列表: []
print("IP地址列表:", host_info[2]) # IP地址列表: ['93.184.216.34']
该函数只适用于IPv4地址,对于IPv6地址,请使用socket.getaddrinfo()
函数。
# 4.通过主机名获取一个完全限定的域名(Fully Qualified Domain Name,FQDN)。
# hostname:要获取其完全限定域名的主机名。
socket.getfqdn(hostname)
import socket
hostname = "www.example.com"
fqdn = socket.getfqdn(hostname)
print("主机名:", hostname) # 主机名: www.example.com
print("完全限定域名:", fqdn) # 完全限定域名: www.example.com
完全限定域名是指包括主机名、域名和顶级域名的完整标识,用于唯一地标识主机在网络中的位置。socket.getfqdn()
函数会使用系统的网络配置和DNS解析,获取指定主机名的完全限定域名。返回的完全限定域名可能是主机名的一部分或与主机名完全相同,具体取决于系统配置和DNS解析结果。
# 5.通过IP地址获取主机名。ip_address:要获取其主机名的IP地址。
# 该函数接受一个IPv4地址作为参数,并返回一个包含主机名、别名列表和IP地址的三元组。
socket.gethostbyaddr(ip_address)
import socket
hostname = "ustc.edu.cn"
host_info = socket.gethostbyname_ex(hostname)
print(host_info) # ('www.example.com', [], ['93.184.216.34'])
ip_address = str(host_info[2])
print(ip_address)
try:
host_name, alias_list, ip_list = socket.gethostbyaddr(ip_address)
print("Host name:", host_name)
print("Alias list:", alias_list)
print("IP list:", ip_list)
except socket.herror as e:
print("Unable to resolve host:", e)
如果无法解析IP地址或找不到对应的主机名,该函数可能引发socket.herror
异常。该函数只适用于IPv4地址,对于IPv6地址,请使用socket.getnameinfo()
函数。socket.gethostbyaddr()
函数执行 DNS 查询来获取主机名和别名。因此,您的计算机必须能够访问 DNS 服务器,并且目标 IP 地址必须具有正确的 DNS 记录才能成功解析主机名。
不知道为什么我在使用这个函数时一直报错:socket.gaierror: [Errno 11001] getaddrinfo failed
# 6.获取主机名和服务名的信息。
# 该函数根据提供的主机名和服务名,返回一个包含多个元组的列表,每个元组包含五个元素,分别表示地址族(family)、套接字类型(socktype)、协议(proto)、规范名称(canonname)和套接字地址(sockaddr)。
socket.getaddrinfo(host, port, family=0, type=0, proto=0, flags=0)
import socket
host = "ustc.edu.cn"
port = 80
try:
addrinfo_list = socket.getaddrinfo(host, port)
for addrinfo in addrinfo_list:
family, socktype, proto, canonname, sockaddr = addrinfo
print("Family:", family)
print("Socket type:", socktype)
print("Protocol:", proto)
print("Canonical name:", canonname)
print("Socket address:", sockaddr)
except socket.gaierror as e:
print("Address resolution failed:", e)
'''
Family: AddressFamily.AF_INET6
Socket type: 0
Protocol: 0
Canonical name:
Socket address: ('2001:da8:d800:642::248', 80, 0, 0)
Family: AddressFamily.AF_INET
Socket type: 0
Protocol: 0
Canonical name:
Socket address: ('202.38.64.246', 80)
'''
host
:主机名或IP地址。port
:端口号。family
:可选参数,指定地址族。type
:可选参数,指定套接字类型。proto
:可选参数,指定协议。flags
:可选参数,指定标志。
# 7.根据服务名获取对应的端口号。
socket.getservbyname(servicename, protocolname=None)
'''
servicename:要获取端口号的服务名,例如http、ftp等。
protocolname:可选参数,指定协议名,例如tcp、udp等。如果未指定,将返回服务名的默认协议的端口号。
'''
import socket
port = socket.getservbyname('http')
print("HTTP端口号:", port) # 输出:HTTP端口号: 80
port = socket.getservbyname('ssh', 'tcp')
print("SSH端口号:", port) # 输出:SSH端口号: 22
2.3绑定socket对象到IP地址
bind()
:将套接字绑定到指定的地址和端口,语法如下:
socket.bind(address)
address
:一个包含主机和端口号的元组,例如('127.0.0.1', 12345)
。
sock = socket.socket()
sock.bind(('localhost', 8000))
sock1 = socket.socket()
sock1.bind((socket.gethostname(), 8001))
sock2 = socket.socket()
sock2.bind(('127.0.0.1', 8001))
2.4服务器端socket开始监听
listen()
:开始监听连接请求,语法如下:
socket.listen(backlog)
backlog
:指定连接请求的最大数量。
2.5连接和接受连接
客户机端socket对象通过 connect()
方法尝试建立到服务器端socket的连接:
client_sock.connect(address)
连接成功时,该函数会返回None
,连接失败时可能引发socket.error
异常。
服务器端socket对象通过accept()
方法进入 waiting
(阻塞)状态。当接收到来自客户端的连接请求时,该函数返回一个新的套接字和客户端地址(元组形式),语法如下:
clientsocket, address = socket.accept()
2.6发送和接收数据
对于面向连接的TCP通信程序,客户端与服务器建立连接以后,通过socket对象的 send()
和 recv()
方法分别发送和接收数据:
socket.send(data, flags=0)
data
:要发送的数据,通常是字节流(bytes)形式。flags
:可选参数,用于指定发送的标志。默认为0,表示没有特殊标志。
如果发送成功,它返回发送的字节数。如果发送失败,它可能引发socket.error
异常。socket.send()
函数是一个阻塞函数,意味着在发送数据期间,程序会一直等待。如果需要非阻塞的发送操作,可以使用socket.settimeout(timeout)
函数设置超时时间,或者使用非阻塞的套接字(通过socket.setblocking(False)
设置)进行发送。
socket.sendall(data, flags=0)
data
:要发送的数据,通常是字节流(bytes)形式。flags
:可选参数,用于指定发送的标志。默认为0,表示没有特殊标志。
与socket.send()
函数不同,socket.sendall()
函数会自动处理数据发送的细节,确保所有数据都被发送完整而不会丢失。如果发送成功,该函数不返回任何值。如果发送失败,它可能引发socket.error
异常。socket.sendall()
函数也是一个阻塞函数。
socket.recv(buffer_size, flags=0)
buffer_size
:接收缓冲区的大小,表示一次接收的最大字节数。flags
:可选参数,用于指定接收的标志。默认为0,表示没有特殊标志。
该函数从已连接的套接字接收数据,并将接收到的数据存储在一个字节流(bytes)中返回。如果没有数据可接收,该函数会阻塞程序,直到有数据到达或连接关闭。如果接收到的数据超过了指定的缓冲区大小,多余的数据可能会在后续的接收中返回。
如果接收成功,该函数返回接收到的字节流。如果接收失败,它可能引发socket.error
异常。socket.recv()
函数也是一个阻塞调用。
对于非面向连接的TCP通信程序,客户机和服务器不需要预先建立连接,直接通过socket对象的 sendto()
方法指定发送目标地址参数,recvfrom()
函数方法返回接受的数据以及发送源地址。
socket.sendto(data, address)
data
:要发送的数据,通常是字节流(bytes)形式。address
:目标地址,表示数据发送的目标主机和端口号,可以是一个元组(host, port)
。
如果发送成功,它返回发送的字节数。如果发送失败,它可能引发socket.error
异常。
socket.recvfrom(buffer_size)
buffer_size
:接收缓冲区的大小,表示一次接收的最大字节数。
该函数从套接字接收数据,并返回一个元组 (data, address)
,其中 data
是接收到的字节流(bytes),address
是数据来源的地址。
如果没有数据可接收,该函数会阻塞程序,直到有数据到达或连接关闭。如果接收到的数据超过了指定的缓冲区大小,多余的数据可能会在后续的接收中返回。
3.简单TCP的网络程序
3.1TCP开发流程
TCP 服务器端步骤:
- 1.创建套接字对象:使用
socket.socket()
函数创建一个套接字对象。 - 2.绑定地址和端口:使用
bind()
函数绑定服务器的地址和端口。 - 3.监听连接:使用
listen()
函数开始监听连接请求。 - 4.接受连接:使用
accept()
函数接受客户端的连接请求,并返回一个新的套接字对象和客户端地址。 - 5.接收数据:使用
recv()
函数接收从客户端发送过来的数据。 - 6.发送响应:使用
send()
函数向客户端发送响应数据。 - 7.关闭连接:关闭客户端的套接字和服务器端的套接字。
TCP 客户端步骤:
- 1.创建套接字对象:使用
socket.socket()
函数创建一个套接字对象。 - 2.目标服务器地址和端口:设置目标服务器的地址和端口。
- 3.连接服务器:使用
connect()
函数连接到服务器。 - 4.发送数据:使用
send()
函数向服务器发送数据。 - 5.接收响应:使用
recv()
函数接收从服务器端发送过来的响应数据。 - 6.关闭连接:关闭客户端的套接字。
3.2TCP服务器端代码实现
import socket
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serversocket.bind(('127.0.0.1', 8001))
serversocket.listen(1) # 开始侦听,队列长度为1
clientsocket, clientaddress = serversocket.accept() # 使用阻塞方法以等待客户机连接请求
print('Connection from ', clientaddress)
while 1:
data = clientsocket.recv(1024)
if not data:
break # 接收到空数据时终止循环
print('Received from client: ', repr(data)) # 输出接收到的数据,用repr()函数转换为字符串
print('Echo: ', repr(data))
clientsocket.send(data)
clientsocket.close() # 关闭客户机socket
serversocket.close() # 关闭服务器socket
3.3TCP客户端代码实现
import socket
clientsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
clientsocket.connect(('127.0.0.1', 8001)) # 连接到服务器
while 1:
data = input('>') # 接收用户输入数据
clientsocket.send(data.encode()) # 数据转换为bytes对象并发送到服务器
if not data:
break # 如果数据为空,终止循环
newdata = clientsocket.recv(1024) # 接收服务器的回送数据
print('Received from server: ', repr(newdata)) # 输出接收到数据
clientsocket.close() # 关闭客户机socket
4.基于UDP的网络程序
4.1UDP开发流程
基于UDP的网络程序是无连接的,发送数据时直接指定地址参数,接收数据时同时返回地址。
UDP 服务器端步骤:
- 1.创建套接字对象:使用
socket.socket()
函数创建一个套接字对象。 - 2.绑定地址和端口:使用
bind()
函数绑定服务器的地址和端口。 - 3.接收数据:使用
recvfrom()
函数接收从客户端发送过来的数据。 - 4.发送响应:使用
sendto()
函数向客户端发送响应数据。 - 5.关闭套接字:关闭服务器端的套接字。
UDP 客户端步骤:
- 1.创建套接字对象:使用
socket.socket()
函数创建一个套接字对象。 - 2.目标服务器地址和端口:设置目标服务器的地址和端口。
- 3.发送数据:使用
sendto()
函数向服务器发送数据。 - 4.接收响应:使用
recvfrom()
函数接收从服务器端发送过来的响应数据。 - 5.关闭套接字:关闭客户端的套接字。
4.2UDP服务器端代码实现
import socket
serversocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
serversocket.bind(('127.0.0.1', 8000))
while 1:
data, address = serversocket.recvfrom(1024) # 接收数据返回数据和客户机地址
if not data: # 接收到空数据终止循环
break
print('Received from client: ', address, repr(data))
print('Echo: ', repr(data))
serversocket.sendto(data, address)
serversocket.close() # 关闭服务器socket
4.3UDP客户端代码实现
import socket
clientsocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
while 1:
data = input('>') # 接收用户输入数据
# 把数据转换为bytes对象,并发送到服务器
clientsocket.sendto(data.encode(), ('127.0.0.1', 8000))
if not data: # 接收到空数据终止循环
break
newdata = clientsocket.recvfrom(1024) # 接收服务器的回送数据
print('Received from server: ', repr(newdata)) # 输出接收到数据
clientsocket.close()