Socket多线程实现服务端与多个客户端通信以及客户端之间的通信

使用Socket实现服务端与客户端通信

Socket

socket一般指套接字,将TCP/IP协议封装为几个简单的接口,应用层调用接口就能实现进程间的通信。通信的两个进程各自持有一个socket,双方通过socket提供的接口进行通信,socket是成对出现的。

socket通信实现过程

服务端创建ServerSocket对象,调用accept()方法监听请求,当接收到请求时,返回一个socket对象。

ServerSocket serverSocket = new ServerSocket(8888);//创建ServerSocket对象时需要绑定一个端口
Socket accept = serverSocket.accept();

客户端创建Socket对象,连接服务端,socket的构造函数包含两个参数,第一个是服务端的ip地址,因为服务端在本机,所以使用localhost,第二个参数是服务端运行的端口,就是在创建ServerSocket对象时绑定的端口。

socket = new Socket("localhost",8888);

客户端和服务端分别通过各自持有的socket对象获取输入流和输出流。

InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();

客户端和服务端可以通过输入流的read()方法发送消息和输出流的write()方法接收消息。

输入流和输出流的简单封装

虽然使用ObjectInputStream和ObjectOutputStream更加方便,但是为了更好的了解通信过程和方便开发,选择对InputSream和OutputStream进行封装,本文只封装了int和String类型。

读取一个int数据,read方法返回一个0-255范围的int数值,即8个bit,int有32个bit,所以需要读取4次,通过位运算整合为一个int变量。

	public static int readInt(InputStream is) {
        int[] values = new int[4];
        try {
            for (int i = 0; i < 4; i++) {
                values[i] = is.read();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        int value = values[0]<<24 | values[1]<<16 | values[2]<<8 | values[3]<<0;
        return value;
    }

发送一个int数据,需要将一个int数据拆分为4个部分,分4次发送。

	public static void writeInt(OutputStream os,int value) {
        int[] values = new int[4];
        values[0] = (value>>24)&0xFF;
        values[1] = (value>>16)&0xFF;
        values[2] = (value>>8)&0xFF;
        values[3] = (value>>0)&0xFF;

        try{
            for (int i = 0; i < 4; i++) {
                os.write(values[i]);
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }

读取一个String字符串,字符串长度不是固定的,read()方法虽然使用-1作为输入结束的标识,但是只有在发送方将输出流关闭时,read()才会接受到-1,否则将一直阻塞,所以需要在发送字符串前发送字符串的长度,并且因为是以byte数组的形式发送的,发送的长度也需要转换为byte数组的长度。接收时使用byte数组接收,并且String提供了将byte数组作为参数的构造方法。

public static String readString(InputStream is) {
    int len = readInt(is);
    byte[] sByte = new byte[len];
    try {
        is.read(sByte);
    } catch (IOException e) {
        e.printStackTrace();
    }
    String s = new String(sByte);
    return s;
}

发送一个String。

public static void writeString(OutputStream os,String s) {
    byte[] bytes = s.getBytes();
    int len = bytes.length;
    writeInt(os,len);
    try {
        os.write(bytes);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

多线程实现服务端与多个客户端同时通信

为了能持续收发消息,客户端与服务端都需要使用死循环,但是没有收发消息的时候程序就会阻塞,因此需要使用多线程,将消息的监听和收发交给一个线程来控制,服务端一个线程用来和一个客户端进行通信。

服务端需要创建一个ServerThread类,继承Thread类,重写run()方法,并且这个类的对象需要持有与客户端通信的socket。

public class ServerThread extends Thread {
    private Socket socket;
    private InputStream is;
    private OutputStream os;

    public ServerThread(Socket socket){
        this.socket = socket;
        try {
            is = socket.getInputStream();
            os = socket.getOutputStream();
        }catch (IOException e){
            e.printStackTrace();
        }
    }

    public Socket getSocket(){
        return socket;
    }
    @Override
    public void run() {
        String infor = socket.getInetAddress()+":"+socket.getPort();
        IOUtil.writeString(os,infor+"你好");
        while (true){
            //等待客户端发送消息
            String msg = IOUtil.readString(is);
            String response = infor+">"+msg;
            IOUtil.writeString(os,response);
        }
    }
}

Server类只需要负责接收连接请求、创建和管理线程。

public class Server {
    private ServerSocket serverSocket = null;
    private HashMap<String,ServerThread> threads = new HashMap<>();
    public void run() throws IOException {
        serverSocket = new ServerSocket(8888);
        System.out.println("服务端启动");
        while (true){
            System.out.println("正在监听");
            Socket accept = serverSocket.accept();
            String address = accept.getInetAddress()+":"+accept.getPort();
            System.out.println("连接成功");
            ServerThread serverThread = new ServerThread(accept);
            threads.put(address,serverThread);
            serverThread.start();
        }
    }
}

客户端,实现一个简单的界面。

public class TestClient extends JFrame {
    private Socket socket;
    private InputStream is;
    private OutputStream os;
    private String response = "";
    private JTextArea jTextArea;

    public void init(){
        setSize(800,600);

        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);
        setTitle("Client");
        setLayout(null);
        setBackground(Color.WHITE);

        Font font = new Font(null,Font.BOLD,16);

        try {
            socket = new Socket("localhost",8888);
            is = socket.getInputStream();
            os = socket.getOutputStream();
        } catch (IOException e) {
            e.printStackTrace();
        }

        jTextArea = new JTextArea();
        jTextArea.setBounds(0,0,800,500);
        jTextArea.setFont(font);

        add(jTextArea);

        response = IOUtil.readString(is);
        jTextArea.setText(response);

        JTextField jTextField = new JTextField();
        jTextField.setFont(font);
        jTextField.setBounds(0,500,800,50);
        add(jTextField);
        jTextField.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                String text = jTextField.getText();
                jTextField.setText("");
                IOUtil.writeString(os,text);
            }
        });

        setVisible(true);

        recive();

    }

    public void recive(){
        while (true){
            String s = IOUtil.readString(is);
            response += "\n"+s;
            jTextArea.setText(response);
        }
    }
}

运行结果

第一句是连接后服务端发送的消息,在最下方输入框输入要发送的内容

在这里插入图片描述

回车发送

在这里插入图片描述

服务端将发送的内容处理后发送回来,发送格式为ip:端口>发送的内容

在这里插入图片描述

两个客户端同时向服务端发送消息

客户端之间的通信

以服务端为中介,可以实现客户端之间的通信。客户端指定发送的消息类型,如私聊、群聊、文件等,指定接收的对象,将这些信息和要发送的内容封装成一个对象发送给服务端,服务端将消息转发给指定的客户端,就能实现客户端之间的通信。为了确定发送的对象,需要使每一个客户端都有唯一的标识,本文为了方便就使用ip地址+端口来标识。

为了便于演示,采用 "消息类型 接收对象 消息内容"三个部分作为输入格式,每个部分之间用空格隔开,实现客户端之间的一对一的通信和向所有客户端的广播。

实现查询客户端列表

在实现之前,可以先设计一个用来查看客户端列表的功能,在客户端向服务端发送指定的字符串,服务端返回所有连接的客户端的列表。我使用“allClients来表示这个指令。

修改Server类和ServerThread类,需要将Server中用于管理全部客户端的列表传给每个thread。

public class Server {
    private ServerSocket serverSocket = null;
    private HashMap<String,ServerThread> threads = new HashMap<>();
    public void run() throws IOException {
        serverSocket = new ServerSocket(8888);
        System.out.println("服务端启动");
        while (true){
            System.out.println("正在监听");
            Socket accept = serverSocket.accept();
            String address = accept.getInetAddress()+":"+accept.getPort();
            System.out.println("连接成功");
            ServerThread serverThread = new ServerThread(accept,threads);

            threads.put(address,serverThread);

            serverThread.start();

        }
    }

    public static void main(String[] args) throws IOException {
        new Server().run();
    }
}
public class ServerThread extends Thread {
    private Socket socket;
    private InputStream is;
    private OutputStream os;
    private HashMap<String,ServerThread> threads;

    public ServerThread(Socket socket,HashMap<String,ServerThread> threads){
        this.socket = socket;
        this.threads = threads;
        try {
            is = socket.getInputStream();
            os = socket.getOutputStream();
        }catch (IOException e){
            e.printStackTrace();
        }
    }

    public Socket getSocket(){
        return socket;
    }
    @Override
    public void run() {
        String infor = socket.getInetAddress()+":"+socket.getPort();
        IOUtil.writeString(os,infor+"你好");
        while (true){
            //等待客户端发送消息
            String msg = IOUtil.readString(is);
            String response = infor+">"+msg;
            if(msg.split(" ")[0].equals("allClients")){
                String users = "";
                Iterator<String> iterator = threads.keySet().iterator();
                while (iterator.hasNext()){
                    String user = iterator.next();
                    if (!user.equals(infor)){
                        users += "\n"+user;
                    }
                }
                response += users;
            }
            IOUtil.writeString(os,response);
        }
    }
}

运行结果

首先运行三个客户端

在这里插入图片描述

实现这个功能后可以更方便地在客户端进行操作。

实现客户端间一对一的通信和广播

同样需要定义命令。使用“sto”作为私聊的命令,即"Send to one"的缩写。使用“sta”作为广播的命令,“send to all”。服务端接收到客户端的消息后,使用splite()方法将字符串按照空格划分,得到一个String数组,取出第一个字符串,判断消息类型,是私聊则取出第二个字符串即接收的客户端,在全部客户端中查询,存在这个客户端就把第三字符串发送过去,不存在就返回一个提示;是广播就遍历客户端发送消息内容(数组中第二个字符串),除去发送的客户端。

修改ServerThread类中的run方法

public void run() {
    String infor = socket.getInetAddress()+":"+socket.getPort();
    IOUtil.writeString(os,infor+"你好");
    while (true){
        //等待客户端发送消息
        String msg = IOUtil.readString(is);
        String response = infor+">"+msg;
        if(msg.split(" ")[0].equals("allClients")){
            String users = "";
            Iterator<String> iterator = threads.keySet().iterator();
            while (iterator.hasNext()){
                String user = iterator.next();
                if (!user.equals(infor)){
                    users += "\n"+user;
                }
            }
            response += users;
        }else if (msg.split(" ")[0].equals("sto")){
            String user = msg.split(" ")[1];
            if (threads.keySet().contains(user)){
                Socket userSocket = threads.get(user).getSocket();
                try {
                    OutputStream userSocketOutputStream = userSocket.getOutputStream();
                    IOUtil.writeString(userSocketOutputStream,infor+"说:"+msg.split(" ")[2]);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }else {
                response = response+"\n用户不存在";
            }
        }else if (msg.split(" ")[0].equals("sta")){
            Iterator<String> users = threads.keySet().iterator();
            while (users.hasNext()){
                String user = users.next();
                if (!user.equals(infor)){
                    Socket userSocket = threads.get(user).getSocket();
                    try {
                        OutputStream userOs = userSocket.getOutputStream();
                        IOUtil.writeString(userOs,infor+"说:"+msg.split(" ")[1]);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        IOUtil.writeString(os,response);
    }
}

运行结果

在这里插入图片描述

在这里插入图片描述

本文的实现方式较为简单,仅提供一个多线程通信的实现思路,实际开发过程需要严格定义通信协议,封装消息对象,消息类型也更多。

完整代码

Server类

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;

public class Server {
    private ServerSocket serverSocket = null;
    private HashMap<String,ServerThread> threads = new HashMap<>();
    public void run() throws IOException {
        serverSocket = new ServerSocket(8888);
        System.out.println("服务端启动");
        while (true){
            System.out.println("正在监听");
            Socket accept = serverSocket.accept();
            String address = accept.getInetAddress()+":"+accept.getPort();
            System.out.println("连接成功");
            ServerThread serverThread = new ServerThread(accept,threads);

            threads.put(address,serverThread);

            serverThread.start();

        }
    }

    public static void main(String[] args) throws IOException {
        new Server().run();
    }
}

ServerThread类

import VedioOnline.Utils.IOUtil;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.HashMap;
import java.util.Iterator;

public class ServerThread extends Thread {
    private Socket socket;
    private InputStream is;
    private OutputStream os;
    private HashMap<String,ServerThread> threads;

    public ServerThread(Socket socket,HashMap<String,ServerThread> threads){
        this.socket = socket;
        this.threads = threads;
        try {
            is = socket.getInputStream();
            os = socket.getOutputStream();
        }catch (IOException e){
            e.printStackTrace();
        }
    }

    public Socket getSocket(){
        return socket;
    }
    @Override
    public void run() {
        String infor = socket.getInetAddress()+":"+socket.getPort();
        IOUtil.writeString(os,infor+"你好");
        while (true){
            //等待客户端发送消息
            String msg = IOUtil.readString(is);
            String response = infor+">"+msg;
            if(msg.split(" ")[0].equals("allClients")){
                String users = "";
                Iterator<String> iterator = threads.keySet().iterator();
                while (iterator.hasNext()){
                    String user = iterator.next();
                    if (!user.equals(infor)){
                        users += "\n"+user;
                    }
                }
                response += users;
            }else if (msg.split(" ")[0].equals("sto")){
                String user = msg.split(" ")[1];
                if (threads.keySet().contains(user)){
                    Socket userSocket = threads.get(user).getSocket();
                    try {
                        OutputStream userSocketOutputStream = userSocket.getOutputStream();
                        IOUtil.writeString(userSocketOutputStream,infor+"说:"+msg.split(" ")[2]);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }else {
                    response = response+"\n用户不存在";
                }
            }else if (msg.split(" ")[0].equals("sta")){
                Iterator<String> users = threads.keySet().iterator();
                while (users.hasNext()){
                    String user = users.next();
                    if (!user.equals(infor)){
                        Socket userSocket = threads.get(user).getSocket();
                        try {
                            OutputStream userOs = userSocket.getOutputStream();
                            IOUtil.writeString(userOs,infor+"说:"+msg.split(" ")[1]);
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
            IOUtil.writeString(os,response);
        }
    }
}

TestClient类

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class TestClient extends JFrame {
    private Socket socket;
    private InputStream is;
    private OutputStream os;
    private String response = "";
    private JTextArea jTextArea;

    public void init(){
        setSize(800,600);

        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);
        setTitle("Client");
        setLayout(null);
        setBackground(Color.WHITE);

        Font font = new Font(null,Font.BOLD,16);

        try {
            socket = new Socket("localhost",8888);
            is = socket.getInputStream();
            os = socket.getOutputStream();
        } catch (IOException e) {
            e.printStackTrace();
        }

        jTextArea = new JTextArea();
        jTextArea.setBounds(0,0,800,500);
        jTextArea.setFont(font);

        add(jTextArea);

        response = IOUtil.readString(is);
        jTextArea.setText(response);

        JTextField jTextField = new JTextField();
        jTextField.setFont(font);
        jTextField.setBounds(0,500,800,50);
        add(jTextField);
        jTextField.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                String text = jTextField.getText();
                jTextField.setText("");
                IOUtil.writeString(os,text);
            }
        });

        setVisible(true);

        recive();

    }

    public void recive(){
        while (true){
            String s = IOUtil.readString(is);
            response += "\n"+s;
            jTextArea.setText(response);
        }
    }

    public static void main(String[] args) {
        TestClient t = new TestClient();
        t.init();
    }
}

IOUtil类

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class IOUtil {
    /**
     * 接收int类型数据
     * @param is 输入流
     * @return 输入流的前四位,整合为一个int数据
     */
    public static int readInt(InputStream is) {
        int[] values = new int[4];
        try {
            for (int i = 0; i < 4; i++) {
                values[i] = is.read();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        int value = values[0]<<24 | values[1]<<16 | values[2]<<8 | values[3]<<0;
        return value;
    }

    /**
     * 输出一个int类型的数据
     * @param os 输出流
     * @param value 要发送的int数据
     */
    public static void writeInt(OutputStream os,int value) {
        int[] values = new int[4];
        values[0] = (value>>24)&0xFF;
        values[1] = (value>>16)&0xFF;
        values[2] = (value>>8)&0xFF;
        values[3] = (value>>0)&0xFF;

        try{
            for (int i = 0; i < 4; i++) {
                os.write(values[i]);
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }

    /**
     * 获取string输入
     * @param is 输入流
     * @return 输入的string
     */
    public static String readString(InputStream is) {
        int len = readInt(is);
        byte[] sByte = new byte[len];
        try {
            is.read(sByte);
        } catch (IOException e) {
            e.printStackTrace();
        }
        String s = new String(sByte);
        return s;
    }

    /**
     * 发送string
     * @param os 输出流
     * @param s 要发送的字符串
     */
    public static void writeString(OutputStream os,String s) {
        byte[] bytes = s.getBytes();
        int len = bytes.length;
        writeInt(os,len);

        try {
            os.write(bytes);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • 24
    点赞
  • 198
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
在Qt中,可以通过QTcpServer类来实现TCP服务器的编写,而多线程则可以通过QThread类来实现。下面是一个简单的示例程序,可以实现多线程多个客户端通信: ```cpp #include <QtNetwork> #include <QtWidgets> #include <QtCore> class ClientThread : public QThread { Q_OBJECT public: explicit ClientThread(qintptr socketDescriptor, QObject *parent = nullptr) : QThread(parent), m_socketDescriptor(socketDescriptor) { } signals: void error(QTcpSocket::SocketError socketError); protected: void run() override { QTcpSocket socket; if (!socket.setSocketDescriptor(m_socketDescriptor)) { emit error(socket.error()); return; } connect(&socket, &QTcpSocket::readyRead, this, &ClientThread::readyRead); connect(&socket, &QTcpSocket::disconnected, this, &ClientThread::disconnected); exec(); } private slots: void readyRead() { QTcpSocket *socket = qobject_cast<QTcpSocket *>(sender()); if (!socket) return; QByteArray data = socket->readAll(); // 处理接收到的数据 socket->flush(); } void disconnected() { QTcpSocket *socket = qobject_cast<QTcpSocket *>(sender()); if (!socket) return; socket->deleteLater(); quit(); } private: qintptr m_socketDescriptor; }; class TcpServer : public QTcpServer { Q_OBJECT public: TcpServer(QObject *parent = nullptr) : QTcpServer(parent) { } protected: void incomingConnection(qintptr socketDescriptor) override { ClientThread *thread = new ClientThread(socketDescriptor, this); connect(thread, &ClientThread::finished, thread, &ClientThread::deleteLater); thread->start(); } }; int main(int argc, char *argv[]) { QApplication app(argc, argv); TcpServer server; if (!server.listen(QHostAddress::Any, 1234)) { qCritical() << "Failed to start server:" << server.errorString(); return 1; } qDebug() << "Server started:" << server.serverAddress().toString() << server.serverPort(); return app.exec(); } #include "main.moc" ``` 在这个示例程序中,TcpServer类继承自QTcpServer类,其中的incomingConnection()函数会在新的客户端连接时被调用。在该函数中,我们创建一个新的ClientThread线程,并将客户端socket描述符传递给它。在ClientThread线程中,我们可以通过QTcpSocket类来与客户端进行通信。当客户端连接断开时,我们需要清理socket并退出线程。 需要注意的是,由于Qt的对象树模型,我们需要在ClientThread线程中使用deleteLater()函数来删除socket对象。这可以确保socket对象不会在其所属的线程销毁之前被销毁。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值