socket是什么?
进程间
数据流的端点操作系统
的通信机制应用程序
通过Socket进行网络数据的传输为了更好的理解Socket,首先要知道一个简单TCP的过程。下面是TCP建立连接时的三次握手的过程:
经过这样的三次握手
的过程,客户端与服务端之间将形成一条通路,此后数据将在这条通路上进行传输。
这与Socket通信过程有什么关系呢?看看Socket建立连接的过程:
客户端与服务端通过Socket程序,经过如上的绑定
、监听
、连接
、确认
等流程,最终会在二者之间建立一条信息传送的通路。
可以发现,Socket建立通信的过程与TCP非常相似。实际上, 这样的Socket使用的就是TCP协议。
而实际上,Socket可以使用的通信方式,不仅是TCP,它能适应多种网络协议,而Socket编程能在多种协议之间形成一种统一的编程方法。其中最主要的网络协议是两种:UDP
和TCP
。
Socekt编程是非常有用的,服务器端传输数据,大量涉及网络协议,而只要涉及网络协议,就离不开Socket应用。
服务端程序: 创建一个Python脚本socekt_server.py
,并进行编辑:
# 导入socket模块
import socket
# 创建实例(不指定参数,会使用默认参数,默认为TCP协议)
skt = socket.socket()
# 绑定监听的ip和port
ip_port = ('127.0.0.1', 8888)
skt.bind(ip_port)
# 设置一个最大连接数(可以没有,而直接进行消息接收)
# 这里的最大连接数是指socket进入监听之后的传入连接数,并不是同一时间有多个程序进行处理,socket是阻塞的,同一时间只有1个程序进行处理
# 即,这里的最大连接数是socket可以挂起的最大连接数,同时有多个请求来,可以进行同时的回应
# 但其中正在处理的程序只有1个,其余处于等待状态,等待正在处理的程序处理完成
# 如果有更多请求,服务器会直接拒绝,该值最小为1
skt.listen(5)
# 接受客户端的连接请求(在接受连接前(客户端发送请求前),accept是阻塞状态,为了体现,加入了一个提示信息,当连接建立,打印客户端的ip和port)
print('正在等待连接...')
conn, address = skt.accept()
print(address)
# 建立连接之后,服务端向客户端返回信息
# 需要注意的是,Python3以上,网络数据的发送接收都是byte类型
# 所以,如果数据是str类型,则需要进行编码
msg = "Hello, I'm server."
conn.send(msg.encode())
# 直接进行连接的关闭,不做其他处理(实际服务端应该针对客户端输入进行处理后再返回数据)
conn.close()
客户端程序: 创建一个Python脚本socekt_server.py
,并进行编辑:
# 导入socket模块
import socket
# 实例初始化,仍然使用默认参数,为TCP连接
# 与服务端使用相同的通信协议,才能建立连接,进行通信
skt = socket.socket()
# 客服端连接的ip和端口必须是服务端所监听的ip和端口
ip_port = ('127.0.0.1', 8888)
skt.connect(ip_port)
# 建立连接之后,接收服务端信息并打印,接受缓冲区中1024个字节的数据
data = skt.recv(1024)
print(data.decode())
# 进行一次接收后即关闭连接
skt.close()
运行后,客户端收到服务端发送来的消息,并打印在命令行上:
而服务端在命令行打印出建立连接的客户端的ip和port:
上面的服务端程序和客户端程序在建立连接后,进行了一次通信之后便终止了,为了实现二者之间连续的消息发送,需要改造服务端和客户端程序。
首先,服务端socekt_server.py
中,对后半部分加上while True
循环,使服务端能够不断地进行连接与断开:
# 导入socket模块
import socket
# 创建实例(不指定参数,会使用默认参数,默认为TCP协议)
skt = socket.socket()
# 绑定监听的ip和port
ip_port = ('127.0.0.1', 8888)
skt.bind(ip_port)
# 设置一个最大连接数(可以没有,而直接进行消息接收)
# 这里的最大连接数是指socket进入监听之后的传入连接数,并不是同一时间有多个程序进行处理,socket是阻塞的,同一时间只有1个程序进行处理
# 即,这里的最大连接数是socket可以挂起的最大连接数,同时有多个请求来,可以进行同时的回应
# 但其中正在处理的程序只有1个,其余处于等待状态,等待正在处理的程序处理完成
# 如果有更多请求,服务器会直接拒绝,该值最小为1
skt.listen(5)
# 不断循环,不断进行连接的建立与关闭
# 这种模拟是合理的,实际情况下,服务端程序不会终止
while True:
# 接受客户端的连接请求(在接受连接前(客户端发送请求前),accept是阻塞状态,为了体现,加入一个提示信息,当连接建立,打印客户端的ip和port)
print('正在等待连接...')
conn, address = skt.accept()
print(address)
# 连接建立之后,服务端向客户端返回信息
# 需要注意的是,Python3以上,网络数据的发送接收都是byte类型
# 所以,如果数据是str类型,则需要进行编码
msg = "Hello, I'm server."
conn.send(msg.encode())
# 直接进行连接的关闭,不做其他处理(实际服务端应该针对客户端输入进行处理后再返回数据)
conn.close()
运行服务端程序,命令行显示:
再运行客户端程序,发现客户端收到了服务端的信息并打印了出来,而后终止:
此时服务端并没有终止,而是打印出客户端的ip和port之后,断开了本次连接,并重新开始等待新的客户端的连接请求:
再次运行客户端,客户端有同样的输出:
服务端则再次打印出了本次连接的客户端的ip和port,然后断开本次连接,并重新等待客户端的下一次连接请求:
现在已经模拟了服务端与客户端不断建立连接、断开连接的过程,仍不能模拟某次连接中,连续发送消息的过程。现在定义的客户端和服务端,是非常简单的:
如果想真正实现服务端与客户端在一次连接之中不断进行通信,需要进一步改造。
终止服务端程序。
服务端socekt_server.py
:
# 导入socket模块
import socket
# 创建服务端socket实例(不指定参数,会使用默认参数,默认为TCP协议)
skt = socket.socket()
# 绑定监听的ip和port
ip_port = ('127.0.0.1', 8888)
skt.bind(ip_port)
# 设置一个最大连接数(可以没有,而直接进行消息接收)
# 这里的最大连接数是指socket进入监听之后的传入连接数,并不是同一时间有多个程序进行处理,socket是阻塞的,同一时间只有1个程序进行处理
# 即,这里的最大连接数是socket可以挂起的最大连接数,同时有多个请求来,可以进行同时的回应
# 但其中正在处理的程序只有1个,其余处于等待状态,等待正在处理的程序处理完成
# 如果有更多请求,服务器会直接拒绝,该值最小为1
skt.listen(5)
# 不断进行连接的建立与关闭
# 这种模拟是合理的,因为实际情况下,服务端程序不会终止,只会有连接的断开
while True:
# 接受客户端的连接请求,在接受连接前,也即客户端发送请求前,accept是阻塞状态
# 为了体现,加入一个提示信息,连接建立后,打印客户端的ip和port
print('正在等待连接...')
conn, address = skt.accept()
print('已与', address, '建立连接!')
# 建立连接之后,服务端向客户端返回信息
# 需要注意的是,Python3以上,网络数据的发送接收都是byte类型
# 所以,如果数据是str类型,则需要进行编码
conn.send("连接成功!".encode())
# 不断接受客户端发送来的数据
while True:
# 接受并打印客户端消息(实际服务端应直接对接收的数据进行处理,而不用打印)
data = conn.recv(1024)
print('From Client:', data.decode())
# 处理客户端发送来的数据
if data == b'exit':
break
# 添加前缀,发送,并额外返回一条信息
response = b'From Server: ' + data
conn.send(response)
conn.send(b'From Server: One more')
# 本次连接关闭
conn.close()
客户端socekt_client.py
:
# 导入socket模块
import socket
# 实例初始化,仍然使用默认参数,为TCP连接
# 与服务端使用相同的通信协议,才能建立连接,进行通信
skt = socket.socket()
# 客服端连接的ip和端口必须是服务端所监听的ip和端口
ip_port = ('127.0.0.1', 8888)
skt.connect(ip_port)
# 连接建立后,服务端会向客户端发送1条数据,进行接收并打印
data = skt.recv(1204)
print(data.decode())
# 不断发送消息
while True:
msg_input = input("输入要发送的消息:")
skt.send(msg_input.encode())
if msg_input == 'exit':
break
# 接收服务端信息并打印,1024指接收缓冲区1024个字节的数据
# 2次接收,因为服务端发送了2次数据
data = skt.recv(1204)
print(data.decode())
data = skt.recv(1204)
print(data.decode())
# 进行一次接收后即关闭连接
skt.close()
启动服务端,服务端显示正在等待连接:
启动客户端,连接建立,客户端收到服务端发送的连接成功的消息,并提示输入消息:
此时服务端显示连接已建立:
此时客户端输入一条消息,点击回车,会收到2条来自服务端的消息,并再次提示输入消息:
服务端则成功收到来自客户端的消息:
再次执行,有相同的结果,客户端:
服务端:
客户端输入exit,客户端便终止了:
服务端则是断开本次连接,重新等待下次连接:
重启一个客户端,客户端:
服务端:
此时如果再启动一个客户端,则新启动的客户端是没有输出的,因为服务端此时正与第一个客户端进行通信,第二个客户端不能与服务端建立连接:
当第1个客户端退出后:
第2个客户端与服务端建立连接:
服务端对应输出:
family:地址簇
type:类型
proto:协议号
具体的,可以看看socket的构造函数:
def __init__(self, family=-1, type=-1, proto=-1, 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.
if fileno is None:
if family == -1:
family = AF_INET
if type == -1:
type = SOCK_STREAM
if proto == -1:
proto = 0
_socket.socket.__init__(self, family, type, proto, fileno)
self._io_refs = 0
self._closed = False
服务端程序: 新建一个名为socket_server_udp.py
的Python脚本,编辑:
import socket
# socket的通信方式为IPv4、数据报(UDP)
skt = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 服务端绑定ip和port
ip_port = ('127.0.0.1', 9999)
skt.bind(ip_port)
# 不断接收数据
while True:
data = skt.recv(1024)
print(data.decode())
# UDP的通信,不需要连接与关闭连接
客户端程序: 新建一个名为socket_client_udp.py
的Python脚本,编辑:
import socket
# socket的通信方式为IPv4、数据报(UDP),与服务端一致
skt = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 目标ip和port
ip_port = ('127.0.0.1', 9999)
# 不断发送数据
while True:
msg_input = input('输入要发送的信息:')
if msg_input == 'exit':
break
# 因为没有建立连接,所以在发送时需要指定ip和port
skt.sendto(msg_input.encode(), ip_port)
# 主动发送关闭信息给服务器端
# UDP并不保证安全通信,所以在一定时间内,客户端不进行通信后,服务端会关闭通信端口
# 不需要显示关闭,但为了资源有效利用,建议直接关闭
skt.close()
启动服务端,没有任何输出:
启动客户端,提示输入信息:
发送一条数据给服务端,客户端:
服务端接收到数据:
再发送一条,客户端:
服务端再次接收到:
再启动一个客户端:
利用新启动的客户端发送一条消息,客户端:
服务端收到了:
此时,终止第1个客户端:
服务端仍在监听:
所以,因为UDP的通信方式不建立连接,所以UDP的通信方式可以进行多次数据发送、多客户端连接的。UDP并不关心接收到的消息是由谁发送的。
可以看到,TCP不能够实现多客户端连接。同一时间,只能有一个正在连接的客户端,其余客户端均处在阻塞状态。
如何解决呢?可以使用socket的非阻塞模块socketserver
,这个模块使用多线程的方式,解决了这个问题。
服务端,新建一个名为socket_server_tcp
的Python脚本,编辑:
import socketserver
import random
class MyServer(socketserver.BaseRequestHandler):
# 如果handle方法出现报错,则会进行跳过
# setup方法和finish方法无论如何都会执行
# 一般只覆盖handle方法
# 首先执行setup
def setup(self):
pass
# 然后执行handle
def handle(self):
# 定义连接变量
conn = self.request
# 发送连接消息
msg = '已连接!'
conn.send(msg.encode())
# 不断接收消息
while True:
data = conn.recv(1024)
print(data.decode())
if data == b'exit':
break
response = b'Server: ' + data
conn.send(response)
conn.send(str(random.randint(1, 1000)).encode())
# 最后执行finish
def finish(self):
pass
if __name__ == '__main__':
# 创建多线程TCPServer实例
server = socketserver.ThreadingTCPServer(('127.0.0.1', 8888), MyServer)
# 开启异步多线程,等待连接
server.serve_forever()
启动,无输出:
再启动一个tcp客户端:
客户端向服务端发送一条消息:
服务端收到了这条消息:
再打开一个客户端:
发送消息给服务端:
服务端收到了第2个客户端发送的消息:
运维的时候经常会遇到需要文件上传的情况,但没有第三方软件,此时可以自己实现。
文件接收: 新建一个名为文件接收.py
的Python脚本,编辑:
# 场景:上传文件到服务器
# 文件接收端是服务器(服务端)
import socket
skt = socket.socket()
ip_port = ('127.0.0.1', 8888)
skt.bind(ip_port)
skt.listen(5)
while True:
# 等待建立连接
conn, address = skt.accept()
# 一直使用当前连接进行数据发送,直到结束标志出现
while True:
with open('file', 'ab') as f:
data = conn.recv(1024)
if data == b'quit':
break
f.write(data)
print('文件接收完成')
conn.close()
文件发送: 新建一个名为文件发送.py
的Python脚本,编辑:
# 场景:上传文件到服务器
# 文件发送端是本机(客户端)
import socket
# 创建socket实例并与服务端建立连接
skt = socket.socket()
ip_port = ('127.0.0.1', 8888)
skt.connect(ip_port)
# 文件分段上传
with open('./my_downloader.py', 'rb') as f:
for i in f:
skt.send(i)
# 传送完成后,非服务端发送结束信号
skt.send('quit'.encode())
先运行文件接收.py
,再运行文件发送.py
,发现文件发送程序很快就终止了,但文件接收程序始终在运行着,说明没有收到结束标志。
再看看收到的文件,最下方是:
也就是说,结束标志被接收在了文件中。
这就涉及了数据的粘包
。因为服务器端接收数据比较慢,而客户端直接将数据进行了发送,所以当服务端收到第一条信息时,客户端可能已经将全部信息发送完成。而服务端每次接收1024字节的数据,可能会将全部的数据进行接收。而此时,全部的数据并不仅有结束标志,所以服务端会认为这是一个数据内容,而进行文件的写入。
如何解决呢?
可以让服务端在每次接收完一段数据后,向客户端发送一个接收成功的标志,而客户端需要等待接收到这个标志,并进行判断,然后再进行下一段的传输。这样就阻止了数据流的大量涌入。
重新编辑文件接收.py
:
# 场景:上传文件到服务器
# 文件接收端是服务器(服务端)
import socket
skt = socket.socket()
ip_port = ('127.0.0.1', 8888)
skt.bind(ip_port)
skt.listen(5)
# 服务端不停机,每接收一个文件建立一个连接
while True:
# 等待建立连接
conn, address = skt.accept()
# 一直使用当前连接进行数据发送,直到结束标志出现
while True:
with open('file', 'ab') as f:
data = conn.recv(1024)
if data == b'quit':
break
f.write(data)
# 每次接收完毕,发送一个接收完成标志
conn.send(b'success')
print('文件接收完成')
conn.close()
重新编辑文件发送.py
:
# 场景:上传文件到服务器
# 文件发送端是本机(客户端)
import socket
# 创建socket实例并与服务端建立连接
skt = socket.socket()
ip_port = ('127.0.0.1', 8888)
skt.connect(ip_port)
# 文件分段上传
with open('./my_downloader.py', 'rb') as f:
for i in f:
skt.send(i)
# 每次发送完毕,等待服务端接收完成标志
data = skt.recv(1024)
if data != b'success':
break
# 传送完成后,非服务端发送结束信号
skt.send('quit'.encode())
这样,依次运行文件接收程序与文件发送程序,得到的结果就是正确的了。文件接收程序不会终止,而只是断开连接并等待新连接的请求。文件发送程序在发送完毕后就终止了。
慕课网 - python运维-Socket网络编程