网络编程之:Socket套接字
Socket
- Socket又称"套接字",用于描述IP地址和端口,是一个通信链的句柄,应用程序通常通过"套接字"向网络发出请求或者应答网络请求.
- socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,对于文件用【打开】【读写】【关闭】模式来操作。socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)
socket和file的区别:
- file模块是针对某个指定文件进行【打开】【读写】【关闭】
- socket模块是针对 服务器端 和 客户端Socket 进行【打开】【读写】【关闭】
1.1 语法格式
socket.socket([family[, type[, proto]]])
**family: **
- socket.AF_INET(IPv4); (默认)
- AF_INET6(IPv6);
- AF_UNIX只能够用于单一的Unix系统进程间通信
type: - 面向连接: SOCK_STREAM(for TCP)(默认)
- 非连接分: SOCK_DGRAM(for UDP)
- SOCK_RAW: 原始套接字,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_RAW可以;其次,SOCK_RAW也可以处理特殊的IPv4报文;此外,利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头。
- SOCK_RDM: 是一种可靠的UDP形式,即保证交付数据报但不保证顺序。SOCK_RAM用来提供对原始协议的低级访问,在需要执行某些特殊操作时使用,如发送ICMP报文。SOCK_RAM通常仅限于高级用户或管理员运行的程序使用。
- SOCK_SEQPACKET 可靠的连续数据包服务
protocol:
- 0(默认)与特定的地址家族相关的协议,如果是 0 ,则系统就会根据地址格式和套接类别,自动选择一个合适的协议
1.2 服务器端套接字
- ser.bind(): 绑定地址(host,port)到套接字, 在AF_INET下,以元组(host,port)的形式表示地址。
- ser.listen(): 开始TCP监听。backlog指定在拒绝连接之前,操作系统可以挂起的最大连接数量。该值至少为1,大部分应用程序设为5就可以了。
- ser.accept(): 被动接受TCP客户端连接,(阻塞式)等待连接的到来
1.3 客户端套接字
- client.connect(): 主动初始化TCP服务器连接,。一般address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。
- client.connect_ex(): connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
1.4 公共用途的套接字函数
- s.recv(): 接收TCP数据,数据以字符串形式返回,bufsize指定要接收的最大数据量。flag提供有关消息的其他信息,通常可以忽略。
- s.send(): 发送TCP数据,将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小。
- s.sendall(): 完整发送TCP数据,完整发送TCP数据。将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。
- s.recvfrom(): 接收UDP数据,与recv()类似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址。
- s.sendto(): 发送UDP数据,将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。
- s.close(): 关闭套接字
- s.getpeername(): 返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)。
- s.getsockname(): 返回套接字自己的地址。通常是一个元组(ipaddr,port)
- s.setsockopt(level,optname,value): 设置给定套接字选项的值。
- s.getsockopt(level,optname[.buflen]): 返回套接字选项的值。
- s.settimeout(timeout): 设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如connect())
- s.gettimeout(): 返回当前超时期的值,单位是秒,如果没有设置超时期,则返回None。
- s.fileno(): 返回套接字的文件描述符。
- s.setblocking(flag): 如果flag为0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)。非阻塞模式下,如果调用recv()没有发现任何数据,或send()调用无法立即发送数据,那么将引起socket.error异常。
- s.makefile(): 创建一个与该套接字相关连的文件
1.5 SocketServer
- 4种类型:
class socketserver.TCPServer(server_address, RequestHandlerClass, bind_and_activate=True)
class socketserver.UDPServer(server_address, RequestHandlerClass, bind_and_activate=True)
class socketserver.UnixStreamServer(server_address, RequestHandlerClass, bind_and_activate=True)
class socketserver.UnixDatagramServer(server_address, RequestHandlerClass,bind_and_activate=True)
+------------+
| BaseServer |
+------------+
|
v
+-----------+ +------------------+
| TCPServer |------->| UnixStreamServer |
+-----------+ +------------------+
|
v
+-----------+ +--------------------+
| UDPServer |------->| UnixDatagramServer |
+-----------+ +--------------------+
- 创建一个socketserver 至少分以下几步:
First, you must create a request handler class by subclassing the BaseRequestHandlerclass and overriding its handle() method; this method will process incoming requests.
Second, you must instantiate one of the server classes, passing it the server’s address and the request handler class.
Then call the handle_request() or serve_forever() method of the server object to process one or many requests.
Finally, call server_close() to close the socket.
- 让你的socketserver并发起来, 必须选择使用以下一个多并发的类
1. 多进程
class socketserver.ForkingTCPServer
class socketserver.ForkingUDPServer
2. 多线程
class socketserver.ThreadingTCPServer
class socketserver.ThreadingUDPServer
将
server = socketserver.TCPServer((HOST, PORT), MyTCPHandler)
换成:
server = socketserver.ThreadingTCPServer((HOST, PORT), MyTCPHandler)
2 实例
2.1 服务端与客户端接收数据
- 客户端
import socket
clinet = socket.socket() # 声明socket类型,同时生成socket连接对象
clinet.connect(('localhost', 6969)) # connect()只接收一个参数,但我们的连接包含IP地址与端口,需要用括号括起来
clinet.send(b"Hello World")
# byte类型只能支持ASCii码的数据类型
# 输出:TypeError: a bytes-like object is required, not 'str' # 默认只支持byte类型
# 若想输入中文,应当使用encode进行编码成byte
clinet.send('你好'.encode('utf-8'))
data = clinet.recv(1024)
print('recv: ', data)
# 输出:recv: b'\xe4\xbd\xa0\xe5\xa5\xbd'
# 此时需要显示中文,需要对编码后的数据进行解码
print('recv: ', data.decode('utf-8'))
# 输出:recv: 你好
clinet.close()
- 服务端
import socket
ser = socket.socket()
ser.bind(('localhost', 6969)) # 绑定要监听的端口
ser.listen() # 监听该端口
print('允许连接')
# ser.accept() # 允许进入
conn, addr = ser.accept() # conn 就是客户端连接过来而在服务器端所生成的一个实例
print(conn, addr)
# <socket.socket fd=592, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 6969), raddr=('127.0.0.1', 5431)> ('127.0.0.1', 5431)
print('准备接收')
data = conn.recv(1024)
print('recv: ', data)
conn.send(data.upper())
ser.close()
2.2 优化1:
-
使客户端可以与服务端进行多次输入互动,但无法实现多客户端同时与服务端建立会话;
-
当客户端断开时,服务端因为收到空的数据,会产生死循环(linux下有效);
-
客户端
import socket
clinet = socket.socket() # 声明socket类型,同时生成socket连接对象
clinet.connect(('localhost', 6969)) # connect()只接收一个参数,但我们的连接包含IP地址与端口,需要用括号括起来
while True: # 在第一次数据发送完毕后,再发送新数据
msg = input('# ').strip()
clinet.send(msg.encode('utf-8'))
data = clinet.recv(1024)
print('recv: ', data.decode('utf-8'))
clinet.close()
- 服务端
import socket
ser = socket.socket()
ser.bind(('localhost', 6969)) # 绑定要监听的端口
ser.listen() # 监听该端口
print('允许连接')
# ser.accept() # 允许进入
conn, addr = ser.accept() # conn 就是客户端连接过来而在服务器端所生成的一个实例
print(conn, addr)
# <socket.socket fd=592, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 6969), raddr=('127.0.0.1', 5431)> ('127.0.0.1', 5431)
while True: # 在第一次接收到数据结束时,再重新开始接收新数据
print('准备接收')
data = conn.recv(1024)
print('recv: ', data)
conn.send(data.upper())
ser.close()
2.3 优化2:
-
允许多客户端与服务端进行多次会话(不能同时), 当前会话断开时,再会与后续请求建立连接
-
但会出现输入空字符卡死的情况。
-
客户端
与上内容不变
- 服务端
import socket
ser = socket.socket()
ser.bind(('localhost', 6969)) # 绑定要监听的端口
ser.listen() # 监听该端口
while True:
print('允许连接')
# ser.accept() # 允许进入
conn, addr = ser.accept() # conn 就是客户端连接过来而在服务器端所生成的一个实例
print(conn, addr)
# <socket.socket fd=592, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 6969), raddr=('127.0.0.1', 5431)> ('127.0.0.1', 5431)
while True:
print('准备接收')
data = conn.recv(1024)
print('recv: ', data)
if not data: # 当接收数据为空,则退出循环
break
conn.send(data.upper())
ser.close()
2.4 优化2:
- 客户端
import socket
clinet = socket.socket() # 声明socket类型,同时生成socket连接对象
clinet.connect(('localhost', 6969)) # connect()只接收一个参数,但我们的连接包含IP地址与端口,需要用括号括起来
while True:
msg = input('# ').strip()
if len(msg) == 0:continue # 注意:socket发不了空,发不了空,发不了空
clinet.send(msg.encode('utf-8'))
data = clinet.recv(1024)
print('recv: ', data.decode('utf-8'))
clinet.close()
- 服务端
与上内容不变
2.5 模拟ssh客户端
- 客户端
import socket
clinet = socket.socket()
clinet.connect(('localhost', 8888))
while True:
msg = input('# ').strip()
if len(msg) == 0:continue
clinet.send(msg.encode('utf-8'))
cmd_res_size = clinet.recv(1024)
clinet.send('ready to recive'.encode('utf-8')) # 客户端回包确认
print(cmd_res_size)
received_size = 0
received_data = b''
while received_size < int(cmd_res_size.decode('utf-8')):
data = clinet.recv(1024)
received_size += len(data) # 每次收到的有可能是小于1024字节,所以必须用len判断
received_data += data
else:
print('cmd res has received done.', received_size)
print(received_data.decode('utf-8'))
clinet.close()
- 服务端
import os
import socket
import time
server = socket.socket()
server.bind(('localhost', 8888))
server.listen()
while True:
conn, addr = server.accept()
while True:
data = conn.recv(1024)
print(data)
if not data:
break
res = os.popen(data.decode('utf-8')).read()
if len(res) == 0:
res = 'cmd has no output...'
conn.send(str(len(res)).encode('utf-8')) # 先发大小给客户端,解决无法将数据一次性返回问题
# time.sleep(0.5)
client_ack = conn.recv(1024) # 等待客户端返回,解决socket粘包问题,否则会在linux下可能会报错
conn.send(res.encode('utf-8'))
server.close()
# 输出:
# python3.8 socket_client.py
# ls
recv: socket_client.py
socket_ser.py
# df -h
recv: Filesystem Size Used Avail Use% Mounted on
udev 959M 0 959M 0% /dev
tmpfs 199M 1.6M 198M 1% /run
/dev/sda1 77G 35G 39G 48% /
tmpfs 993M 0 993M 0% /dev/shm
tmpfs 5.0M 0 5.0M 0% /run/lock
tmpfs 993M 0 993M 0% /sys/fs/cgroup
tmpfs 199M 12K 199M 1% /run/user/131
vmhgfs-fuse 216G 41G 176G 19% /mnt/hgfs
tmpfs 199M 20K 199M 1% /run/user/0
2.6 模拟ftp服务
服务端
import os
import socket
import hashlib
server = socket.socket()
server.bind(('localhost', 2121))
server.listen()
while True:
conn, addr = server.accept()
while True:
data = conn.recv(1024)
if not data:
print('Client is closed')
break
cmd, filename = data.decode().split()
print(filename)
if os.path.isfile(filename):
file_size = os.stat(filename).st_size
conn.send(str(file_size).encode())
conn.recv(1024)
md5_hash = hashlib.md5()
with open(filename, 'rb') as f:
for line in f:
conn.send(line)
md5_hash.update(line)
conn.send(filename + ' md5 is ' + md5_hash.hexdigest())
else:
print('There is no that file')
print('send done')
server.close()
客户端
import socket
import hashlib
md5_hash = hashlib.md5()
client = socket.socket()
client.connect(('192.168.10.11', 2121))
while True:
cmd = input('# ').strip()
if len(cmd) ==0:continue
if cmd.startswith('get'):
client.send(cmd.encode())
rec_file_size = client.recv(1024)
print('the file size is ', rec_file_size)
client.send('read'.encode())
received_size = 0
filename = cmd.split()[1]
with open(filename, 'wb') as f:
while received_size < int(rec_file_size.decode()):
if int(rec_file_size) - received_size > 1024: # 解决粘包问题
size = 1024
else:
size = int(rec_file_size) - received_size
data = client.recv(size)
received_size += len(data)
f.write(data)
md5_hash.update(data)
print(rec_file_size, received_size)
else:
received_md5 = client.recv(1024)
print('received_md5', received_md5)
print('flie received done. and ' + filename + ' md5 is ' + md5_hash.hexdigest())
client.close()
2.7 socketserver
socketserver_server
import socketserver
class MyTCPHandler(socketserver.BaseRequestHandler):
def handle(self):
while True:
try:
self.data = self.request.recv(1024).strip()
print("{} wrote".format(self.client_address[0]))
print(self.data)
self.request.sendall(self.data.upper())
if not self.data:
print(self.client_address[0], "closed!")
break
except ConnectionResetError as error:
print("Error", error)
break
if __name__ == '__main__':
HOST, PORT = "localhost", 9999
server = socketserver.ThreadingTCPServer((HOST, PORT), MyTCPHandler)
server.serve_forever()
socketserver_client
import socket
client = socket.socket()
client.connect(("localhost", 9999))
while True:
msg = input(">>:").strip()
if len(msg) == 0: continue
client.send(msg.encode("utf-8"))
data = client.recv(10240)
print("recv:", data.decode())
client.close()