Python 带有中央中继服务器的多线程套接字

Python 带有中央中继服务器的多线程套接字,python,multithreading,sockets,tkinter,Python,Multithreading,Sockets,Tkinter,我以前曾设法实现了一个客户机-服务器套接字脚本,它在单个客户机和服务器之间传递消息,现在我正在尝试实现一个多客户机系统 更具体地说,我想使用服务器作为两个客户端之间的某种媒介,从一个客户端检索信息并将其转发给另一个客户端。我试图附加并发送接收客户端的端口号,然后从服务器端的消息中提取它。在那之后,我会尝试将它发送到具有该端口号的任何套接字,但我遇到了一些问题(因为端口号是在发送时确定的,我相信?),所以现在我只是尝试将发送的消息中继回所有客户端。但是,问题是消息只发送到服务器,而没有中继到所需的

我以前曾设法实现了一个客户机-服务器套接字脚本,它在单个客户机和服务器之间传递消息,现在我正在尝试实现一个多客户机系统

更具体地说,我想使用服务器作为两个客户端之间的某种媒介,从一个客户端检索信息并将其转发给另一个客户端。我试图附加并发送接收客户端的端口号,然后从服务器端的消息中提取它。在那之后,我会尝试将它发送到具有该端口号的任何套接字,但我遇到了一些问题(因为端口号是在发送时确定的,我相信?),所以现在我只是尝试将发送的消息中继回所有客户端。但是,问题是消息只发送到服务器,而没有中继到所需的客户端

我以前曾试图实现点对点系统,但我遇到了麻烦,所以我决定退后一步,改为这样做

Server.py:

import socket, _thread, threading
import tkinter as tk

SERVERPORT = 8600
HOST = 'localhost'

class Server():
    def __init__(self):
        self.Connected = True
        self.ServerSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.ServerSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR,1)
        self.ServerSocket.bind((HOST, SERVERPORT))
        self.ServerSocket.listen(2)
        self.Clients = []

    def Listen(self):
        print('Server is now running')
        while self.Connected:
            ClientSocket, Address = self.ServerSocket.accept()
            self.Clients.append(Address)
            print('\nNew user connected', Address)
            t = threading.Thread(target=self.NewClient, args=(ClientSocket,
                                                              Address))
            t.daemon = True
            t.start()
        self.Socket.close()

    def NewClient(self, ClientSocket, Address):
        while self.Connected:
            if ClientSocket:
                try:
                    ReceivedMsg = ClientSocket.recv(4096)
                    print('Message received from', Address, ':', ReceivedMsg)
                    self.Acknowledge(ClientSocket, Address)
                    if ReceivedMsg.decode('utf8').split()[-1] != 'message':
                        ReceiverPort = self.GetSendPort(ReceivedMsg)
                        self.SendToClient(ClientSocket,ReceivedMsg,ReceiverPort)
                except:
                    print('Connection closed')
                    raise Exception
        ClientSocket.close()

    def Acknowledge(self, Socket, Address):
        Socket.sendto(b'The server received your message', Address)

    def GetSendPort(self, Msg):
        MsgDigest = Msg.decode('utf8').split()
        return int(MsgDigest[-1])

    def SendToClient(self, Socket, Msg, Port):
        Addr = (HOST, Msg) 
        for Client in self.Clients:
            Socket.sendto(Msg, Client)

def NewThread(Func, *args):
    if len(args) == 1:
        t = threading.Thread(target=Func, args=(args,))
    elif len(args) > 1:
        t = threading.Thread(target=Func, args=args)
    else:
        t = threading.Thread(target=Func)
    t.daemon = True
    t.start()
    t.join()

Host = Server()
NewThread(Host.Listen)
和客户端(.py):

我希望在用户按下tkinter窗口中的send按钮时发送一条消息,但同时,它会持续“监听”以查看是否收到任何消息

我之前还尝试在另一个线程中的客户端中运行
GetResponse
方法,而不是
if self.Connected
我在self.Connected时使用
,但仍然无法工作

更新

在发表了一些有用的评论之后,我对这两个文件进行了如下编辑: 服务器现在为首先运行的每个客户端保留两个套接字。服务器文件作为模块导入到客户机文件中。然后运行每个客户端文件,每个客户端在服务器文件中运行一个函数,请求使用套接字。如果请求是允许的(即没有抛出错误),则连接套接字,将其添加到存储在服务器文件中的一组客户端,然后返回到客户端文件。然后,客户端使用此套接字发送和接收消息

Server.py

import socket, _thread, threading
import tkinter as tk

SERVERPORT = 8600
HOST = 'localhost'

class Server():
    def __init__(self):
        self.Connected = True
        self.ServerSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.ServerSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR,1)
        self.ServerSocket.bind((HOST, SERVERPORT))
        self.ServerSocket.listen(2)
        self.Clients = {}

    def ConnectClient(self, Username, Port):
        Socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.Clients[Username] = [Socket, Port, False]
        try:
            self.Clients[Username][0].connect((HOST, SERVERPORT))
            self.Clients[Username][2] = True
            print('Opened port for user', Username)
            return Socket
        except Exception:
            print('Could not open port for user', Username)
            raise Exception

    def Listen(self):
        print('Server is now running')
        while self.Connected:
            ClientSocket, Address = self.ServerSocket.accept()
            print('\nNew user connected', Address)
            t = threading.Thread(target=self.NewClient, args=(ClientSocket,
                                                              Address))
            t.daemon = True
            t.start()
        self.Socket.close()

    def NewClient(self, ClientSocket, Address):
        while self.Connected:
            if ClientSocket:
                try:
                    ReceivedMsg = ClientSocket.recv(4096)
                    if b'attempting to connect to the server' in ReceivedMsg:
                        ClientSocket.send(b'You are now connected to the server')
                    else:
                        print('Message received from', Address, ':',ReceivedMsg)
                        #self.Acknowledge(ClientSocket, Address)
                        ReceiverPort = self.GetSendPort(ReceivedMsg)
                        if ReceiverPort != None:
                            self.SendToClient(ClientSocket,ReceivedMsg,
                                              ReceiverPort)
                except:
                    print('Connection closed')
                    raise Exception
        ClientSocket.close()

    def Acknowledge(self, Socket, Address):
        Socket.sendto(b'The server received your message', Address)

    def GetSendPort(self, Msg):
        MsgDigest = Msg.decode('utf8').split()
        try:
            Port = int(MsgDigest[-1])
        except ValueError:
            Port = None
        return Port

    def SendToClient(self, Socket, Msg, Port):
        Addr = (HOST, Port)
        Receiver = None
        for Client, Vars in self.Clients.items():
            if Vars[1] == Port:
                Receiver = Client
        self.Clients[Receiver][0].sendto(Msg, Addr)

def NewThread(Func, *args):
    if len(args) == 1:
        t = threading.Thread(target=Func, args=(args,))
    elif len(args) > 1:
        t = threading.Thread(target=Func, args=args)
    else:
        t = threading.Thread(target=Func)
    t.daemon = True
    t.start()
    t.join()

Host = Server()
if __name__ == '__main__':
    NewThread(Host.Listen)
和Client.py

import socket, threading, Server
import tkinter as tk

Username = 'Ernest'
PORT = 8601
OtherPORT = 8602
SERVERPORT = 8600
HOST = '127.0.0.1'

class Client():
    def __init__(self, Username):
        self.Connected, self.Username = False, Username

    def Connect(self):
        print('Requesting to connect to server')
        try:
            self.Socket = Server.Host.ConnectClient(self.Username, PORT)
            self.Connected = Server.Host.Clients[self.Username][2]
            Msg = '{} is attempting to connect to the server'.format(self.Username)
            self.Socket.sendall(bytes(Msg, encoding='utf8'))            
            ReceivedMsg = self.Socket.recv(4096)
            print(ReceivedMsg)
            Msg = MsgUI(self.Username)
            Msg.Display()            
        except Exception:
            print('Could not connect to server')
            raise Exception

    def SendMsg(self):
        try:
            if self.Connected:
                Msg = '{} sent you a message {}'.format(self.Username,OtherPORT)
                self.Socket.sendall(bytes(Msg, encoding='utf8'))
                self.GetResponse()
        except Exception:
            print('Connection closed')
            raise Exception

    def GetResponse(self, *args):
        AckMsg = '\n{} received the message'.format(self.Username)
        NMsg = '\n{} did not receive the message'.format(self.Username)
        if self.Connected:
            Msg = self.Socket.recv(4096)
            print(Msg)
            if Msg:
                self.Socket.sendall(bytes(AckMsg, encoding='utf8'))
            else:
                self.Socket.sendall(bytes(NMsg, encoding='utf8'))

class MsgUI():
    def __init__(self, Username):
        self.Username = Username
        self.entry = tk.Entry(win)
        self.sendbtn = tk.Button(win, text='send', command=Peer.SendMsg)

    def Display(self):
        self.entry.grid()
        self.sendbtn.grid()
        win.mainloop()

win = tk.Tk()
Peer = Client(Username)
Peer.Connect()

现在的问题更多的是python和范围问题。当尝试将消息中继回客户机时,我得到了一个
KeyError
,因为
Clients
字典仍然是空的。在客户端文件中对服务器进行函数调用时,很明显,对字典的更新发生在客户端文件中,而不是服务器文件中,服务器文件位于不同的实例中。我需要一种方法来更改
客户端
字典的内容,该字典由客户端文件调用以执行操作,但在服务器文件中生效。

您是否承诺使用多线程?线程在python中不并发运行(由于GIL),虽然它们是处理并发操作的一种方式,但它们不是唯一的方式,通常也不是最好的方式,除非它们是唯一的方式。考虑这个代码,它不能很好地处理失败案例,但似乎是一个起点。p>
import socket, select, Queue

svrsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
svrsock.setblocking(0)
svrsock.bind(('', 17654))
svrsock.listen(16)
client_queues = {}
write_ready=[] # we'll update this for clients only that have things in the queue
while client_queues.keys() + [svrsock] :
  readable, writable, exceptional = select.select(client_queues.keys() + [svrsock] , write_ready, [])
  for rd in readable:
    if rd is svrsock: # reading listening socket == accepting connection
      conn, addr = svrsock.accept()
      print("Connection from {}".format(addr))
      conn.setblocking(0)
      client_queues[conn] = Queue.Queue()
    else:
      data = rd.recv(1024)
      if data:
        # TODO: send to all queues
        print("Message from {}".format(rd.getpeername()))
        for sock, q in client_queues.iteritems(): 
          q.put("From {}: {}".format( rd.getpeername(), data))
          if sock not in write_ready:
            write_ready.append(sock)
  for rw in writable:
    try:
      data = client_queues[rw].get_nowait()
      rw.send(data)
    except Queue.Empty:
      write_ready.remove(rw)
      continue
这个概念很简单。服务器接受连接;每个连接(套接字)都与一个挂起消息队列相关联。每个准备读取的套接字都从中读取,其消息被添加到每个客户机的队列中。收件人客户端将被添加到具有挂起数据的客户端的
write_ready
列表中(如果尚未在列表中)。然后,每个准备好写入的套接字都会将其下一个排队消息写入其中。如果没有更多的邮件,收件人将从
write\u ready
列表中删除

如果不使用多线程,这非常容易协调,因为所有协调都是应用程序顺序中固有的。对于线程来说,这将更加困难,代码也会多得多,但可能不会因为gil而提高性能

在没有多线程的情况下并发处理多个I/O流的秘密是
select
。原则上这很容易;我们传递了
select()
一个可能用于读取的套接字列表,另一个可能用于写入的套接字列表,以及一个最终列表,对于这个简化的演示,我完全忽略了这个列表。select调用的结果将包括一个或多个实际准备好读取或写入的套接字,这允许我阻止,直到一个或多个套接字准备好进行活动。然后,我处理所有为每次传递活动准备好的套接字(但它们已经被过滤到那些不会阻塞的套接字)

这里还有很多事情要做。我自己不清理,不跟踪关闭的连接,不处理任何异常,等等。但是,不必担心线程和并发性保证,解决这些缺陷非常容易

这里是“行动中”。对于客户端,我使用netcat,它非常适合于没有第4层+协议的第3层测试(换句话说,可以说是原始tcp)。它只需打开一个到给定目标和端口的套接字,通过套接字发送其stdin,并将其套接字数据发送到stdout,这使它非常适合演示此服务器应用程序

我还想指出,在服务器和客户端之间耦合代码是不可取的,因为在不破坏另一个的情况下,您将无法对其中任何一个进行更改。服务器和客户机之间最好有一个“契约”,并对其进行维护。即使在同一代码库中实现服务器和客户端的行为,也应该使用tcp通信契约来驱动实现,而不是代码共享。只有我的2美分,但一旦你开始共享代码,你就会开始以你意想不到的方式耦合服务器/客户端版本

服务器:

$ python ./svr.py
Connection from ('127.0.0.1', 52059)
Connection from ('127.0.0.1', 52061)
Message from ('127.0.0.1', 52061)
Message from ('127.0.0.1', 52059)
Message from ('127.0.0.1', 52059)
第一客户(52059):

第二个客户:

$ nc localhost 17654
From ('127.0.0.1', 52061): hello
hello
From ('127.0.0.1', 52059): hello
hello
From ('127.0.0.1', 52059): hello

如果您需要更详细地理解为什么<代码>选择比并行执行更具吸引力,请考虑:Apache基于线程模型,换句话说,T

$ nc localhost 17654
hello
From ('127.0.0.1', 52061): hello
From ('127.0.0.1', 52059): hello
From ('127.0.0.1', 52059): hello
$ nc localhost 17654
From ('127.0.0.1', 52061): hello
hello
From ('127.0.0.1', 52059): hello
hello
From ('127.0.0.1', 52059): hello