socket是什么
socket又叫套接字,是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等
操作。
Socket是网络通讯经常采用的一种方式,它不是一个具体的物件也不是像http类的通讯协议。你可以把它看成是一组基于TC
P和UDP通信协议的接口,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,
一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
- 简单来说,socket的含义就是两个应用程序通过一个双向的通信连接实现数据的交换,最典型的应用就是程序的客户端和服务端
socket通信的类别
- 这个问题其实也就是说有多少种协议支持socket,这里只讨论tcp和udp
tcp
- 这两种协议都是网络七层中数据传输层的协议,tcp有着三次握手机制,即服务端和客户端每次建立连接需要客户端向服务端发送请求,服务端回应,客户端再次发送方可建立连接,这使得tcp协议不会丢包,数据传输稳定,但是总体连接速度相比较慢
- 基于这种机制,tcp一般应用于资源请求量较小,同时对资源的完整性要求较高的情况中,比如平时访问网页,谁也不希望网站只加载一半,而且只加载左边的一半🤣,同时网页文件较小,所以http协议就是建立在tcp协议的基础上
udp
- udp协议是非连接的,发送数据就是把简单的数据包封装一下,然后从网卡发出去就可以了,数据包之间并没有状态上的联系,正因为udp这种简单的处理方式,导致他的性能损耗非常少,对于cpu,内存资源的占用也远小于tcp,但是对于网络传输过程中产生的丢包,udp并不能保证,所以udp在传输稳定性上要弱于tcp。
- udp的优点是速度快,但是可能产生丢包,所以适用于对实时性要求较高但是对少量丢包并没有太大要求的场景。比如:域名查询,语音通话,视频直播等。
python实现tcp通信
server端
from socket import *
tcp_server = socket(AF_INET,SOCK_STREAM) #把它想成文件操作需要先定义一个指针就不会感觉奇怪了
address = ('127.0.0.1',3434)
tcp_server.bind(address)
tcp_server.listen(5)
- 因为tcp的连接是一对一的,所以一个套接字只能连接一个客户端,但是上面设置了可以同时接收5个客户端的消息,在有客户端使用上面创建好的套接字向服务端通信时要再开一个新的套接字,client_socket是新开的套接字,clientAddr存储了客户端ip
client_socket, clientAddr = tcp_server.accept()
- 接收来自客户端的消息存储到recv_msg,每次接收不超过102400字节,把收到的消息打印出来
recv_msg = client_socket.recv(102400)
print("接收的数据:",recv_msg)
- tcp协议要求收到消息必须发一次回去,不然连接不能继续,所以给套接字发消息
send_data = client_socket.send("这是给您的回复".encode("utf-8"))
client_socket.close()
from socket import *
# 1.创建套接字
tcp_server = socket(AF_INET,SOCK_STREAM)
# 2.绑定ip,port
address = ('127.0.0.1',3434)
tcp_server.bind(address)
# 3.启动被动连接
# 使用socket创建的套接字默认的属性是主动的,使用listen将其变为被动的,这样就可以接收别人的链接了
tcp_server.listen(5) #这个值主要决定了同一时刻有多少个客户端可以连接
# 4.创建接收
# 如果有新的客户端来链接服务器,那么就产生一个新的套接字专门为这个客户端服务
# client_socket用来为这个客户端服务,相当于的tcp_server套接字的代理
# tcp_server_socket就可以省下来专门等待其他新客户端的链接
# 这里clientAddr存放的就是连接服务器的客户端地址
client_socket, clientAddr = tcp_server.accept()
#5.接收对方发送过来的数据
recv_msg = client_socket.recv(102400)#接收1024给字节,这里recv接收的不再是元组,区别UDP
print("接收的数据:",recv_msg)
#6.发送数据给客户端
send_data = client_socket.send("这是给您的回复".encode("utf-8"))
#7.关闭套接字
#关闭为这个客户端服务的套接字,只要关闭了,就意味着为不能再为这个客户端服务了,如果还需要服务,只能再次重新连
client_socket.close()
- 这已经实现了最基本的socket通信,互相发送了一条信息后连接关闭,在此基础上加入for来维持连接(不断重复发送消息和接收消息),加if来判定何时关闭(比如收到客户端发送exit就退出循环),那么一个聊天程序就做好了
客户端
tcp_socket = socket(AF_INET,SOCK_STREAM)
serve_ip = input("请输入服务器ip:")
serve_port = int(input("请输入对应的端口号:")) # 端口要是int类型,所有要转换
tcp_socket.connect((serve_ip,serve_port))
send_data = input("请输入要发送的数据:")
tcp_socket.send(send_data.encode("utf-8"))
tcp_remsg = tcp_socket.recv(102400)
print(tcp_remsg.decode("utf-8"))
tcp_socket.close()
from socket import *
# 1.创建套接字
tcp_socket = socket(AF_INET,SOCK_STREAM)
# 2.准备连接服务器,建立连接
serve_ip = input("请输入服务器ip:")
serve_port = int(input("请输入对应的端口号:")) # 端口要是int类型,所有要转换
tcp_socket.connect((serve_ip,serve_port)) # 连接服务器,建立连接,参数是元组形式
# 3.准备需要传送的数据
send_data = input("请输入要发送的数据:")
tcp_socket.send(send_data.encode("utf-8")) #用的是send方法,不是sendto
#4.从服务器接收数据
tcp_remsg = tcp_socket.recv(102400) #注意这个1024byte,大小根据需求自己设置
print(tcp_remsg.decode("utf-8")) #如果要乱码可以使用tcp_remsg.decode("gbk")
#4.关闭连接
tcp_socket.close()
python实现udp通信
- 仅是客户端和服务端通信,tcp完全够用了,这里讨论的不是客户端和服务端通信的udp,而是两个内网设备利用公网服务端进行udp打洞,实现内网穿透通信的案例
udp打洞原理
当主机A1和B1位于不同的NAT之后,它们用UDP穿越NAT建立直接通信如图3.3所示,具体的过程如下:
(1)A1和B1先登录到公网注册服务器,并向服务器发送各自的实际IP地址和UDP端口信息。
(2)注册服务器记录为两个公网IP地址和端口号,同时从接收到的UDP数据报头中提取IP地址和端口信息,记录为内网IP地址
和端口号,这样在注册服务器上的映射表中就增加两条记录,分别为:A1:(192.168.1.2:2000)一(10.10.10.10:2000)
、B1:(192.168.2.2:4000) 一(20.20.20.20:4000)。
(3)从注册服务器的映射表可以知道A1和B1位于不同的NAT之后。A1要向B1发送UDP消息那么A1发送命令给Server,请
求Server命令B1向A1方向打洞, Server在发送打洞命令时同时将Client A的公网地址10.10.10.10:2000发送给B
1(20.20.20.20:4000)。B1收到命令后向A1的公网地址10.10.10.10:2000发送信息,虽然NAT A会将这个信息丢弃
(因为这样的信息是不请自来的,为了安全,大多数NAT都会执行丢弃动作)。但是我们已经在NAT B上打一个方向
为20.20.20.20:4000(即 A1的外网地址)的洞,那么A1发送到20.20.20.20:4000的信息, B1就能收到了。然后
A1就可以通过B1的外网地址与B1进行P2P通信了。
(4)至此,两个不同NAT之后的主机A1和B1穿越NAT之后,实现了P2P的信息直连。
- 简单的来说(只是打个比方,实际情况中nat网关有多种类型,各种规则,并没有这么简单)
- 两个内网的机器a和b想要通信,但是因为没有公网ip来做唯一地址定位,无法找到对方
- 但是实际上a和b都是有确定的公网位置的,只不过没有公网IP
- nat通过端口映射将不同端口映射到不同的内网ip,比如a是192.168.1.1,对应了11.22.33.44这个公网ip的1145端口
- b是192.168.1.1,对应了公网ip22.33.44.55的514端口
- a连接到公网服务器c,把自己11.22.33.44:1145告诉c
- b连接到c,把自己的22.33.44.55:514告诉c
- c再把公共ip和端口告诉对方,双方就可以通过公网ip和端口号建立了连接(实际上比这复杂的多)
udp打洞示例
服务端
import logging
import socket
logger = logging.getLogger()
addresses = []
def addr_to_msg(addr):
return '{}:{}'.format(addr[0], str(addr[1])).encode('utf-8')
def main(host='0.0.0.0', port=9999):
sock = socket.socket(socket.AF_INET, # Internet
socket.SOCK_DGRAM) # UDP
sock.bind((host, port))
while True:
data, addr = sock.recvfrom(1024) # buffer size is 1024 bytes
logger.info("connection from: %s", addr)
addresses.append(addr)
if len(addresses) >= 2:
logger.info("server - send client info to: %s", addresses[0])
sock.sendto(addr_to_msg(addresses[1]), addresses[0])
logger.info("server - send client info to: %s", addresses[1])
sock.sendto(addr_to_msg(addresses[0]), addresses[1])
addresses.pop(1)
addresses.pop(0)
if __name__ == '__main__':
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
main()
客户端1
import logging
import socket
import threading
logger = logging.getLogger()
def msg_to_addr(data):
ip, port = data.decode('utf-8').strip().split(':')
return (ip, int(port))
def send_to(sock,addr):
while 1:
a=input(f"\r向{addr}发送:").encode()
sock.sendto(a, addr)
def to_recv(sock):
while 1:
data, addr1 = sock.recvfrom(1024)
print('\r 接收信息: 地址:{} 内容:{}'.format(addr1, data))
def main(host='127.0.0.1', port=9999):
sock = socket.socket(socket.AF_INET, # Internet
socket.SOCK_DGRAM) # UDP
sock.sendto(b'0', (host, port))
print(f"向服务器{host}发送")
while True:
print("开始等待接收1")
data, addr = sock.recvfrom(1024)
print('1、接收: 地址:{} 内容:{}'.format(addr, data))
addr = msg_to_addr(data)
print(f"2、获取新地址{addr}地址并开始发送")
sock.sendto(b'0', addr)
break
t2 = threading.Thread(target=to_recv, args=(sock,))
t2.start()
t1 = threading.Thread(target=send_to, args=(sock, addr,))
t1.start()
if __name__ == '__main__':
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
main()
客户端2
import logging
import socket
import threading
logger = logging.getLogger()
def msg_to_addr(data):
ip, port = data.decode('utf-8').strip().split(':')
return (ip, int(port))
def send_to(sock,addr):
while 1:
a=input(f"\r向{addr}发送:").encode()
sock.sendto(a, addr)
def to_recv(sock):
while 1:
data, addr1 = sock.recvfrom(1024)
print('\r 接收信息: 地址:{} 内容:{}'.format(addr1, data))
def main(host='127.0.0.1', port=9999):
sock = socket.socket(socket.AF_INET, # Internet
socket.SOCK_DGRAM) # UDP
sock.sendto(b'0', (host, port))
print(f"向服务器{host}发送")
while True:
print("开始等待接收1")
data, addr = sock.recvfrom(1024)
print('1、接收: 地址:{} 内容:{}'.format(addr, data))
addr = msg_to_addr(data)
print(f"2、获取新地址{addr}地址并开始发送")
sock.sendto(b'0', addr)
break
t2 = threading.Thread(target=to_recv, args=(sock,))
t2.start()
t1 = threading.Thread(target=send_to, args=(sock, addr,))
t1.start()
if __name__ == '__main__':
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
main()
基于udp的文件传输
接收者(需要先开)
import socket
import tqdm
import os
import threading
# 使用UDP传输视频,全双工,但只需一方接,一方收即可
# 设置服务器的ip和 port
# 服务器信息
# sever_host = '127.0.0.1'
# sever_port =1234
def recvived(address, port):
# 传输数据间隔符
SEPARATOR = '<SEPARATOR>'
# 文件缓冲区
Buffersize = 4096*10
while True:
print('准备接收新的文件...')
udp_socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
udp_socket.bind((address, port))
recv_data = udp_socket.recvfrom(Buffersize)
recv_file_info = recv_data[0].decode('utf-8') # 存储接收到的数据,文件名
print(f'接收到的文件信息{recv_file_info}')
c_address = recv_data[1] # 存储客户的地址信息
# 打印客户端ip
print(f'客户端{c_address}连接')
# recv_data = udp_socket.recv()
# 接收客户端信息
# received = udp_socket.recvfrom(Buffersize).decode()
filename ,file_size = recv_file_info.split(SEPARATOR)
# 获取文件的名字,大小
filename = os.path.basename(filename)
file_size = int(file_size)
# 文件接收处理
progress = tqdm.tqdm(range(file_size), f'接收{filename}', unit='B', unit_divisor=1024, unit_scale=True)
with open('8_18_'+filename,'wb') as f:
for _ in progress:
# 从客户端读取数据
bytes_read = udp_socket.recv(Buffersize)
# 如果没有数据传输内容
# print(bytes_read)
if bytes_read == b'file_download_exit':
print('完成传输!')
print(bytes_read)
break
# 读取写入
f.write(bytes_read)
# 更新进度条
progress.update(len(bytes_read))
udp_socket.close()
if __name__ == '__main__':
# address = ("127.0.0.1", 1234)
port = 1234
address = "127.0.0.1"
t = threading.Thread(target=recvived, args=(address, port))
t.start()
# send(address)
发送者
# import socket
# import tqdm
# import os
# import threading
#
# # 由客户端向服务器传数据,文件
import threading
import socket
import tqdm
import os
import cv2
from time import ctime, sleep
def send(address, filename):
# 传输数据间隔符
SEPARATOR = '<SEPARATOR>'
# 服务器信息
host, port = address
# 文件缓冲区
Buffersize = 4096*10
# 传输文件名字
filename = filename
# 文件大小)
file_size = os.path.getsize(filename)
# 创建socket链接
s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
print(f'服务器连接中{host}:{port}')
s.connect((host, port))
print('与服务器连接成功')
# 发送文件名字和文件大小,必须进行编码处理
# s.sendto(f'{filename}{SEPARATOR}{file_size}'.encode(), ("127.0.0.1", 1234))
s.send(f'{filename}{SEPARATOR}{file_size}'.encode('utf-8'))
# 文件传输
progress = tqdm.tqdm(range(file_size), f'发送{filename}', unit='B', unit_divisor=1024)
with open(filename, 'rb') as f:
# 读取文件
for _ in progress:
bytes_read = f.read(Buffersize)
# print(bytes_read)
if not bytes_read:
print('exit退出传输,传输完毕!')
s.sendall('file_download_exit'.encode('utf-8'))
break
# sendall 确保络忙碌的时候,数据仍然可以传输
s.sendall(bytes_read)
progress.update(len(bytes_read))
sleep(0.001)
# 关闭资源
s.close()
if __name__ == '__main__':
address = ('127.0.0.1', 20742)
# host = '127.0.0.1'
# port = 1234
filename = input('请输入文件名:')
t = threading.Thread(target=send, args=(address, filename))
t.start()
# received(address, filename)
udp打洞实现文件传输
- 我们实现了udp打洞的文本传输,我们也实现了基于udp的文件传输,那么我们是否可以实现基于udp打洞的点对点文件传输呢?我做出了以下尝试,但是还未完成它
服务端
import logging
import socket
logger = logging.getLogger()
addresses = []
def addr_to_msg(addr):
return '{}:{}'.format(addr[0], str(addr[1])).encode('utf-8')
def main(host='0.0.0.0', port=9999):
sock = socket.socket(socket.AF_INET, # Internet
socket.SOCK_DGRAM) # UDP
sock.bind((host, port))
while True:
data, addr = sock.recvfrom(1024) # buffer size is 1024 bytes
logger.info("connection from: %s", addr)
addresses.append(addr)
if len(addresses) >= 2:
logger.info("server - send client info to: %s", addresses[0])
sock.sendto(addr_to_msg(addresses[1]), addresses[0])
logger.info("server - send client info to: %s", addresses[1])
sock.sendto(addr_to_msg(addresses[0]), addresses[1])
addresses.pop(1)
addresses.pop(0)
if __name__ == '__main__':
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
main()
接收文件客户端
import socket
import tqdm
import os
import threading
import requests
import time
# 使用UDP传输视频,全双工,但只需一方接,一方收即可
# 设置服务器的ip和 port
# 服务器信息
# sever_host = '127.0.0.1'
# sever_port =1234
def recvived(address, port):
# 传输数据间隔符
SEPARATOR = '<SEPARATOR>'
# 文件缓冲区
Buffersize = 4096*10
while True:
print('准备接收新的文件...')
udp_socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
udp_socket.bind((address, port))
recv_data = udp_socket.recvfrom(Buffersize)
recv_file_info = recv_data[0].decode('utf-8') # 存储接收到的数据,文件名
print(f'接收到的文件信息{recv_file_info}')
c_address = recv_data[1] # 存储客户的地址信息
# 打印客户端ip
print(f'客户端{c_address}连接')
# recv_data = udp_socket.recv()
# 接收客户端信息
# received = udp_socket.recvfrom(Buffersize).decode()
filename ,file_size = recv_file_info.split(SEPARATOR)
# 获取文件的名字,大小
filename = os.path.basename(filename)
file_size = int(file_size)
# 文件接收处理
progress = tqdm.tqdm(range(file_size), f'接收{filename}', unit='B', unit_divisor=1024, unit_scale=True)
with open('8_18_'+filename,'wb') as f:
for _ in progress:
# 从客户端读取数据
bytes_read = udp_socket.recv(Buffersize)
# 如果没有数据传输内容
# print(bytes_read)
if bytes_read == b'file_download_exit':
print('完成传输!')
print(bytes_read)
break
# 读取写入
f.write(bytes_read)
# 更新进度条
progress.update(len(bytes_read))
udp_socket.close()
if __name__ == '__main__':
# address = ("127.0.0.1", 1234)
#port = 1234
#address = "127.0.0.1"
time.sleep(20)
a=requests.request('get',"http://127.0.0.1/udpp.txt")
address=a[0:a.rfind(':', 1)]
port=a[a.rfind(':'):]
t = threading.Thread(target=recvived, args=(address, port))
t.start()
# send(address)
发送客户端
import logging
import socket
import threading
import threading
import socket
import requests
import tqdm
import os
import cv2
from time import ctime, sleep
logger = logging.getLogger()
def msg_to_addr(data):
ip, port = data.decode('utf-8').strip().split(':')
return (ip, int(port))
def send_to(sock,addr):
while 1:
a=input(f"\r向{addr}发送:").encode()
sock.sendto(a, addr)
def to_recv(sock):
while 1:
data, addr1 = sock.recvfrom(1024)
print('\r 接收信息: 地址:{} 内容:{}'.format(addr1, data))
def send(address, filename):
# 传输数据间隔符
SEPARATOR = '<SEPARATOR>'
# 服务器信息
host, port = address
# 文件缓冲区
Buffersize = 4096*10
# 传输文件名字
filename = filename
# 文件大小)
file_size = os.path.getsize(filename)
# 创建socket链接
s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
print(f'服务器连接中{host}:{port}')
s.connect((host, port))
print('与服务器连接成功')
# 发送文件名字和文件大小,必须进行编码处理
# s.sendto(f'{filename}{SEPARATOR}{file_size}'.encode(), ("127.0.0.1", 1234))
s.send(f'{filename}{SEPARATOR}{file_size}'.encode('utf-8'))
# 文件传输
progress = tqdm.tqdm(range(file_size), f'发送{filename}', unit='B', unit_divisor=1024)
with open(filename, 'rb') as f:
# 读取文件
for _ in progress:
bytes_read = f.read(Buffersize)
# print(bytes_read)
if not bytes_read:
print('exit退出传输,传输完毕!')
s.sendall('file_download_exit'.encode('utf-8'))
break
# sendall 确保络忙碌的时候,数据仍然可以传输
s.sendall(bytes_read)
progress.update(len(bytes_read))
sleep(0.001)
# 关闭资源
s.close()
def main(host='127.0.0.1', port=9999):
sock = socket.socket(socket.AF_INET, # Internet
socket.SOCK_DGRAM) # UDP
sock.sendto(b'0', (host, port))
print(f"开始连接服务器{host}")
if True:
print("打洞成功")
data, addr = sock.recvfrom(1024)
print('对方临时地址为:{}'.format(data))
requests.request("get",'http://127.0.0.1/udpp.php?%s'%data)
address = (data)
# host = '127.0.0.1'
# port = 1234
filename = input('请输入文件名:')
t = threading.Thread(target=send, args=(address, filename))
t.start()
# received(address, filename)