通信编程之java socket【二】-持续通信和多线程通信

上一节讲到java socket的服务端和客户端的简单通信,了解到socket的通信机制,详见:通信编程之java socket初探

今天我们继续深入一下,之前的例子有一个问题,就是只能发送一次消息就结束了,我们知道微信、QQ都是持续的收发消息的,那我们怎么才能使客户端持续的发送消息呢?下面我们就来实战探讨下。

一、java socket怎么持续通信

socket的服务端是阻塞式的通信的,通过accept()方法来阻塞,等待客户端的连接,连接后客户端发送消息,通过IO来收发消息。从这个流程上来看,我们持续的执行这个动作,那么就能收到客户端的消息了,大家想到了,是的,服务端通过while(true)来控制循环收发消息。我们来看下服务端的代码:

package socketStudy;

import java.io.*;
import java.net.*;

/**
 * socket 服务端
 * @author xiaoming
 * @version 1.0
 * @date 2022-01-28
 */
public class CommunicationServer {

    public static String socketserver_ip = "127.0.0.1";
    public static int socketserver_port = 8881;

    public static void main(String[] args) throws IOException {

       startSocketForSimp();

    }

    /**
     * 简单的socket服务端,可以连接一个客户的端,持续通信
     */
    public static void startSocketForSimp(){

        try {
            ServerSocket ss = new ServerSocket(socketserver_port);
            System.out.println("CommunicationServer启动服务器....端口为:"+socketserver_port+" wait connect...");

            Socket s = ss.accept();
            System.out.println("收到客户端连接,客户端:"+s.getInetAddress().getHostAddress()+"已连接到服务器");

            //持续读取和发送消息
            readAndWriteMsg(s.getInputStream(),s.getOutputStream());

        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    /**
     * 读取和写入消息
     * @param inp
     * @param outp
     * @throws IOException
     */
    public static void readAndWriteMsg(InputStream inp,OutputStream outp) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(inp));
        //持续读取客户端发送来的消息
        while(true) {
            String mess = br.readLine();
            System.out.println("【收到客户端信息】信息为:" + mess);

            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(outp));
            bw.write("【服务端】已收到客户端发送消息,消息为:"+mess+"\n");
            bw.flush();
        }

    }
}

从以上代码可以看出,当一个客户端连接后,进入while(true)循环中,通过br.readLine()持续的读取客户端的消息,打印到控制台上。再看下客户端的代码:

package moreClientAndThread;

import socketStudy.CommunicationServer;

import java.io.*;
import java.net.Socket;
import java.net.UnknownHostException;

/**
 * socket客户端代码
 * @author xiaoming
 * @version 1.0
 * @date 2022-02-05
 */
public class ClientSocket1 {
        public static void main(String[] args) {
        try {
            //连接socket服务端
            Socket s = new Socket(CommunicationServer.socketserver_ip,CommunicationServer.socketserver_port);

            //构建IO
            InputStream inp = s.getInputStream();//输入流,收到的信息
            OutputStream outp = s.getOutputStream();//输出流,发出去的消息

            //从控制台获取消息,向服务器端发送一条消息
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in,"UTF-8"));
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(outp));
            while(true){
                String str = bufferedReader.readLine();//从控制台读取消息
                bw.write(str);
                bw.write("\n");//表示一条信息结束了,服务端通过
                bw.flush();

                //读取服务器返回的消息
                BufferedReader br = new BufferedReader(new InputStreamReader(inp));
                String mess = br.readLine();
                System.out.println("【收到服务器信息】:"+mess);
        }

        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

从客户端代码中可以看到,通过从控制台上持续的接受输入的消息,发送到服务端,我们来看下运行的效果:

服务端运行结果:

收到客户端连接,客户端:127.0.0.1已连接到服务器
【收到客户端信息】信息为:1-123
【收到客户端信息】信息为:1-qwea

客户端运行结果:

1-123
【收到服务器信息】:【服务端】已收到客户端发送消息,消息为:1-123
1-qwea
【收到服务器信息】:【服务端】已收到客户端发送消息,消息为:1-qwea

从而实现了客户端持续的发送消息。但是上面的代码也有个问题,因为我们通过while阻塞在那里了,所以新的客户端连结过来的时候是连接不上的,从而只能连接一个客户端。如果我们要实现类似微信这样的通信,还需要实现多客户端同时发送消息。那么我们怎么实现多客户端通信呢?

二、java socket怎么多客户端通信

如果要实现多个客户端同时连接并通信,那么我们有什么办法呢?我们分析一下服务端的处理流程,有两个核心点,一个是接受客户端的连接accept(),一个是连接上之后,持续的读取客户端你的消息,者两个地方是冲突的,所以我们需要将两者分离。分离的办法就是,接收到一个新的客户端连接之后,起一个新的线程来处理这个客户端的消息的的读取,和主线程分离,从而在服务端产生多线程,每一个客户端是一个独立的子线程。总结下这个方法,就是在服务端的主线程中处理客户端的连接,在子线程中处理客户端的消息读取。

我们来看下服务端的代码:

package socketStudy;

import java.io.*;
import java.net.*;

/**
 * socket 服务端
 * @author xiaoming
 * @version 1.0
 * @date 2022-01-28
 */
public class CommunicationServer {

    public static String socketserver_ip = "127.0.0.1";
    public static int socketserver_port = 8881;

    public static void main(String[] args) throws IOException {

        startSocketForMoreThread();
    }


    /**
     * 多线程通信socket服务端
     */
    public static void startSocketForMoreThread() throws IOException {
        ServerSocket ss = new ServerSocket(socketserver_port);
        System.out.println("CommunicationServer启动服务器....端口为:"+socketserver_port+" wait connect...");

        while(true){
            Socket s = ss.accept();
            System.out.println("收到客户端连接,客户端:"+s.getInetAddress().getHostAddress()+"已连接到服务器");
            //起一个线程处理
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        //读取和写入消息
                        readAndWriteMsg(s.getInputStream(),s.getOutputStream());
                    } catch (IOException e) {
                        e.printStackTrace();
                    }

                }
            }).start();
        }
    }

    /**
     * 读取和写入消息
     * @param inp
     * @param outp
     * @throws IOException
     */
    public static void readAndWriteMsg(InputStream inp,OutputStream outp) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(inp));
        //持续读取客户端发送来的消息
        while(true) {
            Thread t = Thread.currentThread();
            String tname = t.getName();
            String mess = br.readLine();
            System.out.println("线程name="+tname+"【收到客户端信息】信息为:" + mess);

            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(outp));
            bw.write("【服务端】已收到客户端发送消息,消息为:"+mess+"\n");
            bw.flush();
        }

    }
}

从服务端代码可以看出,我们将accept()和消息的读写分离了,客户端消息读写单独起来一个线程来进行处理,我们特地将线程的name打印出来,来区分多线程处理的情况,从而更直观的看到多线程的处理过程。我们再看下客户端的代码,我们写了两个客户端类ClientSocket1:

package moreClientAndThread;

import socketStudy.CommunicationServer;

import java.io.*;
import java.net.Socket;
import java.net.UnknownHostException;

/**
 * socket客户端代码
 * @author xiaoming
 * @version 1.0
 * @date 2022-02-05
 */
public class ClientSocket1 {
        public static void main(String[] args) {
        try {
            //连接socket服务端
            Socket s = new Socket(CommunicationServer.socketserver_ip,CommunicationServer.socketserver_port);

            //构建IO
            InputStream inp = s.getInputStream();//输入流,收到的信息
            OutputStream outp = s.getOutputStream();//输出流,发出去的消息

            //从控制台获取消息,向服务器端发送一条消息
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in,"UTF-8"));
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(outp));
            while(true){
                String str = bufferedReader.readLine();//从控制台读取消息
                bw.write(str);
                bw.write("\n");//表示一条信息结束了,服务端通过
                bw.flush();

                //读取服务器返回的消息
                BufferedReader br = new BufferedReader(new InputStreamReader(inp));
                String mess = br.readLine();
                System.out.println("【收到服务器信息】:"+mess);
        }

        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

再写一个客户端2的类,ClientSocket2,代码可以是一样的,ClientSocket2的代码我就不贴出来了。

我们先启动服务端代码,再依次启动客户端代码,我们看下实际运行的效果,如下:

服务端运行结果:

"C:\Program Files\Java\jdk1.8.0_311\bin\java.exe" ...
CommunicationServer启动服务器....端口为:8881 wait connect...
收到客户端连接,客户端:127.0.0.1已连接到服务器
线程name=Thread-0【收到客户端信息】信息为:1-123
线程name=Thread-0【收到客户端信息】信息为:1-qwe
收到客户端连接,客户端:127.0.0.1已连接到服务器
线程name=Thread-1【收到客户端信息】信息为:2-qwe
线程name=Thread-1【收到客户端信息】信息为:2-qw1

客户端1的运行结果:

"C:\Program Files\Java\jdk1.8.0_311\bin\java.exe" ...
1-123
【收到服务器信息】:【服务端】已收到客户端发送消息,消息为:1-123
1-qwe
【收到服务器信息】:【服务端】已收到客户端发送消息,消息为:1-qwe

客户端2的运行结果:

"C:\Program Files\Java\jdk1.8.0_311\bin\java.exe" ...
2-qwe
【收到服务器信息】:【服务端】已收到客户端发送消息,消息为:2-qwe
2-qw1
【收到服务器信息】:【服务端】已收到客户端发送消息,消息为:2-qw1

从以上可以看到我们实现了多客户端的连接,并通信。

三、遇到的问题及解决办法

在实际代码编写和调试的环节遇到了一些问题,做下记录分享。

1、多线程的的使用

多线程的使用,大家都学过,但是实际项目中并不一定使用过,时间长了也容易忘记,我在使用的时候,又复习了下多线程的用法,发现多线程的知识还是很深的,包扩进程和线程的关系,多线程的启动,线程池,线程间通信,线程处理的结果返回等等,这个后续我单独写一篇文章分享一下。

我本次使用的是new Thread(new Runnable(){})在Runnable里面重写了run()方法,运行后都正常,但是就是没有执行run()方法里面的消息收发逻辑,排查了一遍后发现,没有调用.start()方法,因为没有报错,排查起来有点绕,对于长期不写多线程的伙伴容易出现这个问题。正确用法要如下,不要忘记调用 .start()方法。

new Thread(new Runnable(){
                @Override
                public void run(){
                       //TODO:
                                  }
           }).start()

2、将代码整理成多个方法,抽取公共处理逻辑

在写代码的过程中,由于多个逻辑切换,代码揉在一个方法中,导致每次改动都在一个方法中改动,容易出错,浪费时间,后来将整体流程分为客户端连接,消息的收发处理,然后再main函数中进行调用,这样每次改动的时候只影响一个方法里面的逻辑,从而大大减少了出错的次数,调试时间也大大减少,处理逻辑也更清晰了,代码量也少了很多。

还有将一些常量抽出来进行复用,也可减少代码量,减少出错的概率,养成好习惯。

3、Connection reset问题

这个问题比较复杂,网上很多资料都说不明白,我也没研究明白,后续继续研究,争取写一篇专栏探讨Connection reset问题及其解决办法。

你可能感兴趣的:(网络通信编程,java,开发语言,后端,多线程,socket)