Python中,我们利用Socket套接字来实现网络通信,可以说套接字是实现网络编程进行数据传输的一种技术手段。Socket用于描述IP地址和端口,应用程序通常通过"套接字"向网络发出请求或者应答网络请求。
Socket主要是基于应用层和传输层之间,是一个中间的抽象层,功能是将负责的复杂的TCP/IP协议族隐藏在Socket接口后面。
应用程序通过套接字发送或接收数据,socket模块针对服务器端和客户端Socket进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合。
通给指定地址簇和socket类型来进行创建。
地址簇 | 描述 |
---|---|
socket.AF_UNIX | 只能够用于单一的Unix系统进程间通信 |
socket.AF_INET | 服务器之间网络通信IPv4 |
socket.AF_INET6 | IPv6 |
socket类型 | 描述 |
---|---|
socket.SOCK_STREAM | 流式socket , for TCP |
socket.SOCK_DGRAM | 数据报式socket , for UDP |
socket.SOCK_RAW | 原始套接字,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_RAW可以;其次,SOCK_RAW也可以处理特殊的IPv4报文;此外,利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头。 |
服务器与客户端交互过程:
服务器:
1 创建socket套接字
2 绑定地址和端口
3 监听客户端socket请求
4 等待客户端连接
5 创建新套接字描述符,等待客户端发送请求
客户端:
1 创建套接字
2 发送请求,连接服务器地址和端口
3 连接成功后,发送/接收数据
socket常用函数 | 描述 |
---|---|
sk.bind(address) | 将socket绑定到地址。address地址的格式取决于地址族。在AF_INET下,以元组(host,port)的形式表示地址。 |
sk.listen(backlog) | 开始监听传入连接。backlog指定在拒绝连接之前,可以挂起的最大连接数量。 |
sk.connect(address) | 连接到address处的socket。一般,address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。 |
sk.connect_ex(address) | 同上,只不过会有返回值,连接成功时返回 0 ,连接失败时候返回编码。 |
sk.close() | 关闭socket。 |
sk.recv(bufsize) | 接受socket的数据。数据以字符串形式返回,bufsize指定最多可以接收的数量。 |
sk.send(string) | 将string中的数据发送到连接的socket。 |
sk.sendall(string) | 将string中的数据发送到连接的socket,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。 |
sk.settimeout(timeout) | 设置socket操作的超时期,timeout是一个浮点数,单位是秒。超时期应该在刚创建socket时设置. |
sk.accept() | 接受连接并返回(conn,address),其中conn是新的socket对象,可以用来接收和发送数据。address是连接客户端的地址。 |
服务器:
import socket
from loggers import log
# 1 创建socket套接字
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 格式socket.socket(地址簇,socket类型)
# 返回一个通信套接字s,为本机向网络通信的接口。
# 2 绑定地址和端口
port = 18080
host = socket.gethostname()
# 绑定本地地址,只有主机上的进程可以连接到服务器,如果host传空字符串,服务器将接受本机所有可用的 IPv4 地址。
s.bind((host, port))
# 3 等待客户端连接
s.listen(3)
# 建立最多三个连接监听,在拒绝连接之前,操作系统可以挂起的最大连接数量。
# 4 连接成功后,发送/接收数据
while True:
# (阻塞式)等待连接的到来
conn, addr = s.accept()
# conn是新的套接字对象,可以用来接收和发送数据。address是连接客户端的地址。
log.info("欢迎{}连接".format(addr))
while True:
data = conn.recv(1024)
dt = data.decode('utf-8')
log.info("服务器收到{}".format(dt))
log.info("服务器发送:")
p = input()
if p == 'quit':
conn.close()
s.close()
else:
conn.send(p1.encode('utf-8'))
客户端:
import sys
import socket
from loggers import log
def client():
log.info("客户端启动")
# 1 创建套接字
c = socket.socket()
port = 18080
host = socket.gethostname()
# 2 连接地址和端口
c.connect((host, port))
# 3 连接成功后,发送/接收数据
while True:
log.info("客户端发送:")
a = input()
if a == 'quit':
c.close()
sys.exit(0)
else:
c.send(a.encode('utf-8'))
data = c.recv(1024)
log.info("客户端收到:{}".format(data))
if __name__ == '__main__':
client()
实例一实现了一个客户端与服务器的通信,但存在一些问题:不能多个客户端同时与服务器通信;发送和接收不能同时进行等。通过使用多线程,就可以解决一些问题。
服务器:
class MyServer(object):
def __init__(self):
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
host = socket.gethostname()
port = 18080
self.server.bind((host, port))
self.server.listen(5)
# 将客户端连接的socket,addr保存为键值对,放在字典中。
self.socket_mapping = {}
self.socket_list = []
self.maxSize = 1024
self.loc = threading.Lock()
def run(self):
"""
创建用户连接线程和发送信息线程
"""
t2 = threading.Thread(name="client02", target=my_server.creat_client_socket, args=())
t2.start()
t1 = threading.Thread(name="client01", target=my_server.send_to_client, args=())
t1.start()
def send_to_client(self):
"""
获取键盘输入并发送给客户端
:param socket:
:return:
"""
while True:
info = input('输入发送内容\n')
print("您要发送的内容为:%s" % info)
id = input('请选择需要发送的线程ID:0~' + str(len(self.socket_list) - 1) + '\n')
print('输入的id为:', id)
self.socket_list[int(id)].send(info.encode('utf-8'))
def create_client_socket(self):
"""
连接客户端,开启连接客户端线程
"""
while True:
soc, addr = self.server.accept()
soc.send('success!'.encode('utf-8'))
self.socket_list.append(soc)
print("新的客户端", soc)
threading.Thread(target=self.recv_from_client, args=(soc, addr)).start()
def recv_from_client(self, soc, addr):
"""
接收客户端信息
:param socket:
:return:
"""
while True:
addr = addr
recv_info = soc.recv(self.maxSize).decode('utf-8')
print('服务器收到客户端{}的信息: {}'.format(addr, recv_info))
客户端:
import socket
import threading
from loggers import log
class MyClient:
def __init__(self):
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
host = socket.gethostname()
port = 18080
self.server.connect((host, port))
self.maxSize = 1024
# 将收和发分别创建线程,可以解决收发不能同时进行的问题
def run(self):
"""
同时收发信息
"""
tr = threading.Thread(target=self.text_recv)
tr.start()
ts = threading.Thread(target=self.text_send)
ts.start()
# 收和发分别封装成函数
def text_recv(self):
"""
接收服务端发来的信息
"""
while True:
data = self.server.recv(1024)
dt = data.decode('utf-8')
log.info("客户端收到{}".format(dt))
def text_send(self):
"""
发送信息给服务器
"""
while True:
try:
msg = input()
self.server.sendall(msg.encode('utf8'))
if msg == 'quit':
self.server.close()
except Exception as e:
log.error(e)
问题记录:
如下是服务器存在问题的一部分,连接多个客户端,并同时开启发送和接收线程。
但在发送数据时,存在问题。假设有两个客户端0和1同时连接,在进行send_to_client时,想通过输入来选择发送给哪一个客户端,input()阻塞式输入,会一直等待输入,输入选择第0个客户端后,不继续往下执行之后的代码,会再执行客户端1的 “选择客户端”,然后再切换到客户端0的发送线程继续执行之后的输入信息,再切换到客户端1的发送线程继续执行。这样达不到选择客户端,并进行信息发送的目的。
上述正确实现中,将发送信息线程单独拿出来,连接的多个客户端创建为多个线程,发送信息时,选择客户端线程发送信息。
def run(self):
"""
多线程同时收发信息
"""
while True:
socket, addr = self.server.accept()
log.info("{}连接成功".format(addr))
socket.send('success!'.encode('utf-8'))
self.socket_mapping[socket] = addr
threading.Thread(name="client01", target=self.send_to_client, args=(socket,)).start()
threading.Thread(name="client02", target=self.recv_from_client, args=(socket,)).start()
def send_to_client(self):
"""
获取键盘输入,发送给客户端
:param
:return:
"""
while True:
socket_list = list(self.socket_mapping.keys())
addr_list = list(self.socket_mapping.values())
log.info("addr_list:{}".format(addr_list))
print(len(addr_list))
if len(self.socket_mapping) > 1:
choose = input("选择客户端:")
for i in range(len(addr_list)):
print(i)
# if self.loc.acquire(True):
# info = input("输入发送给客户端:")
# if info != "quit" and len(info) > 0:
# socket_list[i].send(info.encode("utf-8"))
# else:
# socket_list[i].close()
# self.loc.release()