JAVA利用Socket和ServerSocket实现一对多通信

JAVA利用Socket和ServerSocket实现一对多通信

  • 一、文章目的
  • 二、手工创建线程实现多线程通信
    • 1、服务端实现
    • 2、客户端实现
    • 3、接收消息类
    • 4、发送消息
  • 三、基于线程池实现
    • 1、客户端实现
    • 2、服务端
      • 1、二次修改服务端效果图
      • 服务端再次调整
  • 四、结束语
  • 五、借鉴文章

一、文章目的

闲暇之余,突然想了解java分布式应用,在读**《分布式java应用基础与实践》**一书时说到,分布式应用各系统之间通信方式可以使用基于消息方式的系统间通信,其中基于java包的TCP/IP通信就有Socket、ServerSocket,故特地网上搜索相关文章学习。本文借鉴网络一些文章,结合自己摸索两天的所得的粗略心得所写,也是本人在CSDN的第一篇文章,多有不足之处,望不小心读到的大神们斧正。写本文目的主要在于记录自己的学习路程,记录学习socket中遇到的一些易错点,以便未来某一天若忘记了方便查看,若是有幸能帮助到读到文章的您也是非常的好。

二、手工创建线程实现多线程通信

1、服务端实现

package com.nmj.controller;

import com.nmj.service.ReceiveService;

import java.io.*;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Scanner;

/**
 * 服务端
 * @author NMJ
 */
public class Server {
    public static ServerSocket serverSocket;
    Socket socket;
    ArrayList<Socket> sockets = new ArrayList<>();
    public Server(){
        try {
            //1、绑定端口之前应先设置相应参数,否则设置不起作用
            serverSocket = new ServerSocket();
            //启用端口重用
            serverSocket.setReuseAddress(true);
            serverSocket.bind(new InetSocketAddress(8080));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public void start(){
        System.out.println("服务器启动....");
        while(true){
            try{
                socket = serverSocket.accept();
                sockets.add(socket);
                System.out.println("创建新的连接:"+socket.getPort());
                //接收消息
                Thread recevieThread = new Thread(new ReceiveService(socket));
                recevieThread.start();

                /*//创建新线程发送消息(单个)
                Thread senThread =  new Thread(new SendService(socket));
                senThread.start();*/
                //群体发送消息
                new SendBatch().start();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
    public class SendBatch extends Thread{
        @Override
        public void run() {
            OutputStream outputStream;
            PrintWriter printWriter;
            try{
                Scanner scanner;scanner = new Scanner(System.in);
                String msg;
                /**
                 * 2、要加此条件,否则只能发送一条消息
                 */
                while(true){
                    /**
                     * 3、scanner.nextLine()要放在while里面,否则会无限次发送
                     */
                    msg = scanner.nextLine();
                    for (Socket socket1 :sockets){
                        outputStream = socket1.getOutputStream();
                        printWriter = new PrintWriter(outputStream,true);
                        /**
                         *  4、此处应使用println,或者调用socket.shutdownOutput()作为接受消息传递的标志,否则一直阻
                         *   塞起,shutdownOutput()只是关闭输出流,而不是关闭socket本身,将此方法放于一个流的末端,
                         *   表示已经传完东西了,无需再等候接受消息了
                         */
                        printWriter.println("发送给客户端"+socket1.getPort()+"的消息:"+msg);

                    }
                }

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

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


2、客户端实现

package com.nmj.controller;

import com.nmj.service.ReceiveService;
import com.nmj.service.SendService;

import java.net.Socket;

/**
 * 客户端(可实现多线程)
 * @author NMJ
 */
public class Client {
    Socket socket;
    private String message;
    /**
     * 服务端IP,若是本地可用"localhost"或者"127.0.0.1"
     */
    String uri = "127.0.0.1";
    /**
     * 服务端端口
     */
    int serverPort = 8080;

    public Client(String message){
        this.message = message;
        try{
            //连接服务器
            socket = new Socket(uri,serverPort);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public Client(){
        try{
            //连接服务器
            socket = new Socket(uri,serverPort);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    public void start(){
        System.out.println("客户端启动....");
        /*//创建新线程发送消息(控制台交互)
        Thread senThread =  new Thread(new SendService(socket));
        senThread.start();*/
        //外部传入消息
        Thread sendThread = new Thread(new SendService(socket,message));
        sendThread.start();
        //创建新线程接收消息
        Thread receiveThread = new Thread(new ReceiveService(socket));
        receiveThread.start();
    }
    public static void main(String[] args) {
        /*//控制台交互
        new Client().start();*/
        String message = "我是客户端发送的消息";
        new Client(message).start();
    }
}


3、接收消息类

说明:将接受消息和发送消息分开来做单独的线程主要是为了保证能够同时发送和接收消息,否则只能按顺序进行

package com.nmj.service;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Socket;

/**
 * 接收消息
 * @author NMJ
 */
public class ReceiveService implements Runnable{
    private Socket socket;

    public ReceiveService(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        InputStream inputStream;
        InputStreamReader inputStreamReader;
        BufferedReader bufferedReader;
        try{
            inputStream = socket.getInputStream();
            inputStreamReader = new InputStreamReader(inputStream);
            bufferedReader = new BufferedReader(inputStreamReader);
            String result = "";
            while((result = bufferedReader.readLine()) != null){
                System.out.println("消息:" + result);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

4、发送消息

package com.nmj.service;


import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;


/**
 * 发送消息
 * @author NMJ
 */
public class SendService implements Runnable{
    private Socket socket;
    private String message;
    public SendService(Socket socket){
        this.socket = socket;
    }

    public SendService(Socket socket,String message){
        this.socket = socket;
        this.message = message;
    }

    @Override
    public void run() {
        OutputStream outputStream;
        PrintWriter printWriter;
        try{
            outputStream = socket.getOutputStream();
            printWriter = new PrintWriter(outputStream,true);
            /*控制台交互
            Scanner sc = new Scanner(System.in);
            String msg;
            while(true){
                msg = sc.nextLine();
                printWriter.println(msg);
            }*/
            printWriter.println(message);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

三、基于线程池实现

一谈到通信,一般来说都会伴随着多线程的出现,可以用以上的手工创建线程实现,但是利用手工创建线程存在很大的弊端,频繁的创建和销毁线程会使得效率变得很低,而且不利于管理,故考虑用线程池来解决。目前考虑采用java自带的ThreadPoolExecutor来实现,话不多说,直接上代码

1、客户端实现

package com.nmj.controller;

import com.nmj.service.ReceiveService;
import com.nmj.service.SendService;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.*;

/**
 * 客户端
 * @author NongMaoJun
 */
public class Client1 {
    Socket socket;
    ThreadPoolExecutor poolExecutor;
    /**
     * 线程数
     */
    private int poolSize = 2;
    /**
     * 线程最大容量
     */
    private int maxPoolSize = 10;
    /**
     * 线程等待时间
     */
    private int keepAliveTime = 200;
    private int queueSize = maxPoolSize - poolSize;
    /**
     * 服务端IP,若是本地可用"localhost"或者"127.0.0.1"
     */
    String uri = "127.0.0.1";
    /**
     * 服务端端口
     */
    int serverPort = 8080;

    public Client1(){
        /**
         * 使用ArrayBlockingQueue有界任务队列,若有新的任务需要执行时,线程池会创建新的线程,直到创建的线程数量达到poolSize时,则会将新的任务加入到等待队列中。若等待队列已满,
         * 即超过ArrayBlockingQueue初始化的容量,则继续创建线程,直到线程数量达到maxPoolSize设置的最大线程数量,若大于maxPoolSize,则执行拒绝策略。在这种情况下,
         * 线程数量的上限与有界任务队列的状态有直接关系,如果有界队列初始容量较大或者没有达到超负荷的状态,线程数将一直维持在poolSize以下,反之当任务队列已满时,
         * 则会以maxPoolSize为最大线程数上限。
         */
        poolExecutor = new ThreadPoolExecutor(poolSize, maxPoolSize, keepAliveTime, TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<>(queueSize), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
    }

    public class ClientThread implements Runnable{
        @Override
        public void run() {
            try {
                socket = new Socket(uri,serverPort);
                System.out.println("客户端socket:"+socket);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }


    public void start(){
        try {
            poolExecutor.execute(new ClientThread());
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println("客户端启动....");
        //创建新线程发送消息
        Scanner scanner = new Scanner(System.in);
        if(!StringUtils.isEmpty(scanner)){
            poolExecutor.execute(new SendService(socket, new Scanner(System.in).toString()));
        }
        //创建新线程接收消息
        poolExecutor.execute(new ReceiveService(socket));
    }

    public static void main(String[] args) {
        new Client1().start();
    }

}

2、服务端

package com.nmj.controller;


import com.nmj.service.ReceiveService;
import com.nmj.service.SendService;
import org.springframework.http.converter.json.GsonBuilderUtils;
import org.w3c.dom.ls.LSOutput;

import java.io.IOException;
import java.net.InetSocketAddress;

import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 服务端
 */
public class Server1 {
    ServerSocket  serverSocket;
    Socket socket;
    ThreadPoolExecutor poolExecutor;
    /**
     * 线程数
     */
    private int poolSize = 2;
    /**
     * 线程最大容量
     */
    private int maxPoolSize = 10;
    /**
     * 线程等待时间
     */
    private int keepAliveTime = 200;
    private int queueSize = maxPoolSize - poolSize;


    public Server1(){
        /**
         * 使用ArrayBlockingQueue有界任务队列,若有新的任务需要执行时,线程池会创建新的线程,直到创建的线程数量达到poolSize时,则会将新的任务加入到等待队列中。若等待队列已满,
         * 即超过ArrayBlockingQueue初始化的容量,则继续创建线程,直到线程数量达到maxPoolSize设置的最大线程数量,若大于maxPoolSize,则执行拒绝策略。在这种情况下,
         * 线程数量的上限与有界任务队列的状态有直接关系,如果有界队列初始容量较大或者没有达到超负荷的状态,线程数将一直维持在poolSize以下,反之当任务队列已满时,
         * 则会以maxPoolSize为最大线程数上限。
         */
        poolExecutor = new ThreadPoolExecutor(poolSize, maxPoolSize, keepAliveTime, TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<>(queueSize), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());

    }
    public class AcceptSocket implements Runnable{
        @Override
        public void run() {
            try {
                //1、绑定端口之前应先设置相应参数,否则设置不起作用
                serverSocket = new ServerSocket();
                //启用端口重用
                serverSocket.setReuseAddress(true);
                serverSocket.bind(new InetSocketAddress(8080));
                socket = serverSocket.accept();
                //接收消息
                poolExecutor.execute(new ReceiveService(socket));
                //创建新线程发送消息(单个)
                poolExecutor.execute(new SendService(socket));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public void start(){
        System.out.println("服务器启动....");
        try{
            poolExecutor.execute(new AcceptSocket());
        }catch (Exception e){
            e.printStackTrace();
        }
    }


    public static void main(String[] args) {
         new Server1().start();
    }

}

再次实验,多线程的服务端实际上存在问题,只能发消息给一个客户端,接收消息没问题,于是又修改了一下,代码如下

package com.nmj.controller;


import com.baomidou.mybatisplus.extension.api.R;
import com.nmj.service.ReceiveService;
import com.nmj.service.SendService;
import org.springframework.http.converter.json.GsonBuilderUtils;
import org.springframework.util.StringUtils;
import org.w3c.dom.ls.LSOutput;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetSocketAddress;

import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Scanner;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 服务端
 */
public class Server1 {
    ServerSocket  serverSocket;
    Socket socket;
    ThreadPoolExecutor poolExecutor;
    ArrayList sockets = new ArrayList<>();
    /**
     * 线程数
     */
    private int poolSize = 10;
    /**
     * 线程最大容量
     */
    private int maxPoolSize = 20;
    /**
     * 线程等待时间
     */
    private int keepAliveTime = 200;
    private int queueSize = maxPoolSize - poolSize;


    public Server1(){
        /**
         * 使用ArrayBlockingQueue有界任务队列,若有新的任务需要执行时,线程池会创建新的线程,直到创建的线程数量达到poolSize时,则会将新的任务加入到等待队列中。若等待队列已满,
         * 即超过ArrayBlockingQueue初始化的容量,则继续创建线程,直到线程数量达到maxPoolSize设置的最大线程数量,若大于maxPoolSize,则执行拒绝策略。在这种情况下,
         * 线程数量的上限与有界任务队列的状态有直接关系,如果有界队列初始容量较大或者没有达到超负荷的状态,线程数将一直维持在poolSize以下,反之当任务队列已满时,
         * 则会以maxPoolSize为最大线程数上限。
         */
        poolExecutor = new ThreadPoolExecutor(poolSize, maxPoolSize, keepAliveTime, TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<>(queueSize), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());

    }
    public class AcceptSocket implements Runnable{
        @Override
        public void run() {
            try {
                //1、绑定端口之前应先设置相应参数,否则设置不起作用
                serverSocket = new ServerSocket();
                //启用端口重用
                serverSocket.setReuseAddress(true);
                serverSocket.bind(new InetSocketAddress(8080));
                /**
                 * 试试去掉while看能不能发送消息,实验证明去掉只能发送消息给第一个客户端,故不能去掉,否则创建连接时,除了第一个客户端后面的客户端将不会执行到括弧中内容,
                 * 但是加上之后,从服务端发送消息发送不出去了
                 */
                while(true){
                    socket = serverSocket.accept();
                    sockets.add(socket);
                    System.out.println("创建新的连接"+ socket.getPort());
                    System.out.println("当前服务端线程数:"+poolExecutor.getPoolSize());
                    //接收消息
                    poolExecutor.execute(new ReceiveService(socket));
                    //Scanner scanner = new Scanner(System.in);
                    //服务器群发消息没成功
                    //多个客户端开启后单个服务端发送消息也是没有发送成功
                    /*for (Socket socket1 :sockets){
                        System.out.println("socket1:"+socket1);
                        poolExecutor.execute(new SendService(socket1,scanner.toString()));
                        System.out.println("我要群发消息");
                    }*/
                    //一次性用线程群发,行不通
                    //poolExecutor.execute(new SendBatch(scanner));
                    new Server1.SendBatch().start();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public class SendBatch extends Thread {
        @Override
        public void run() {
            OutputStream outputStream;
            PrintWriter printWriter;
            try{
                Scanner scanner;scanner = new Scanner(System.in);
                String msg;
                /**
                 * 2、要加此条件,否则只能发送一条消息
                 */
                while(true){
                    /**
                     * 3、scanner.nextLine()要放在while里面,否则会无限次发送
                     */
                    msg = scanner.nextLine();
                    for (Socket socket1 :sockets){
                        outputStream = socket1.getOutputStream();
                        printWriter = new PrintWriter(outputStream,true);
                        /**
                         *  4、此处应使用println,或者调用socket.shutdownOutput()作为接受消息传递的标志,否则一直阻
                         *   塞起,shutdownOutput()只是关闭输出流,而不是关闭socket本身,将此方法放于一个流的末端,
                         *   表示已经传完东西了,无需再等候接受消息了
                         */
                        printWriter.println("发送给客户端"+socket1.getPort()+"的消息:"+ msg);

                    }
               }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
    public void start(){
        System.out.println("服务器启动....");
        try{
            poolExecutor.execute(new AcceptSocket());
        }catch (Exception e){
            e.printStackTrace();
        }
    }


    public static void main(String[] args) {
         new Server1().start();
    }

}

1、二次修改服务端效果图

JAVA利用Socket和ServerSocket实现一对多通信_第1张图片JAVA利用Socket和ServerSocket实现一对多通信_第2张图片这里又发现了一个问题,服务端在客户端发送消息之前发送消息的话,客户端实际上是接收不到的,这点就有点蒙圈了,等待研究更新吧

服务端再次调整

心累啊,经过多次修改、调整,终于发现问题所在,群发时,群发消息只能开辟一个线程去实现,否则就会出现需要多次发送才能从服务端发出消息(比如有2个线程,那么从服务端发出消息需要第二次发送才能发出,并且发出的内容时缓存里面的所有内容,包含第一次发送的内容),修改后代码如下:

package com.nmj.controller;


import com.baomidou.mybatisplus.extension.api.R;
import com.nmj.service.ReceiveService;
import com.nmj.service.SendService;
import org.springframework.http.converter.json.GsonBuilderUtils;
import org.springframework.util.StringUtils;
import org.w3c.dom.ls.LSOutput;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetSocketAddress;

import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Scanner;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 服务端
 */
public class Server1 {
    ServerSocket  serverSocket;
    Socket socket;
    ThreadPoolExecutor poolExecutor;
    ArrayList sockets = new ArrayList<>();
    /**
     * 线程数
     */
    private int poolSize = 10;
    /**
     * 线程最大容量
     */
    private int maxPoolSize = 20;
    /**
     * 线程等待时间
     */
    private int keepAliveTime = 200;
    private int queueSize = maxPoolSize - poolSize;

    /**
     * 是否群发
     */
    private boolean isGroup = true;


    public Server1(){
        /**
         * 使用ArrayBlockingQueue有界任务队列,若有新的任务需要执行时,线程池会创建新的线程,直到创建的线程数量达到poolSize时,则会将新的任务加入到等待队列中。若等待队列已满,
         * 即超过ArrayBlockingQueue初始化的容量,则继续创建线程,直到线程数量达到maxPoolSize设置的最大线程数量,若大于maxPoolSize,则执行拒绝策略。在这种情况下,
         * 线程数量的上限与有界任务队列的状态有直接关系,如果有界队列初始容量较大或者没有达到超负荷的状态,线程数将一直维持在poolSize以下,反之当任务队列已满时,
         * 则会以maxPoolSize为最大线程数上限。
         */
        poolExecutor = new ThreadPoolExecutor(poolSize, maxPoolSize, keepAliveTime, TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<>(queueSize), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
        try {
            //1、绑定端口之前应先设置相应参数,否则设置不起作用
            serverSocket = new ServerSocket();
            //启用端口重用
            serverSocket.setReuseAddress(true);
            serverSocket.bind(new InetSocketAddress(8080));
        }catch(Exception e){
            e.printStackTrace();
        }

    }
    public class AcceptSocket implements Runnable{
        @Override
        public void run() {
            try {
                /**
                 * 试试去掉while看能不能发送消息,实验证明去掉只能发送消息给第一个客户端,故不能去掉,否则创建连接时,除了第一个客户端后面的客户端将不会执行到括弧中内容,
                 * 但是加上之后,从服务端发送消息发送不出去了
                 */
                while(true){
                    socket = serverSocket.accept();
                    sockets.add(socket);
                    System.out.println("创建新的连接"+ socket.getPort());
                    System.out.println("当前服务端线程数:"+poolExecutor.getPoolSize());
                    //接收消息
                    poolExecutor.execute(new ReceiveService(socket));
                    System.out.println("是不是没来这里");
                    //服务器群发消息
                    //用线程群发,行不通
                    //poolExecutor.execute(new SendBatch());
                    /**
                     * 目前此法可以,但是是当前服务端有几个线程,就必须发起几次才能发出去,比如说服务端两个线程,那么消息只有第二次发送才能从服务端发出
                     * 此后也是第2的倍数此次发送才能发送成功
                     */
                    //new Server1.SendBatch().start();
                    /**
                     * 加上此判断,群发时只能开辟一个线程,否则出现以上情况
                     */
                    if(isGroup){
                        isGroup = false;
                        new Server1.SendBatch().start();
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public class SendBatch extends Thread {
        @Override
        public void run() {
            OutputStream outputStream;
            PrintWriter printWriter;
            try{
                Scanner scanner = new Scanner(System.in);
                String msg;
                System.out.println("进入群发trye");
                /**
                 * 2、要加此条件,否则只能发送一条消息
                 */
                while(true){//放在外层判断
                    /**
                     * 3、scanner.nextLine()要放在while里面,否则会无限次发送
                     */
                    msg = scanner.nextLine();
                    System.out.println("进入群发while");
                    for (Socket socket1 :sockets){
                        System.out.println("第N个socket"+socket1);
                        outputStream = socket1.getOutputStream();
                        printWriter = new PrintWriter(outputStream,true);
                        /**
                         *  4、此处应使用println,或者调用socket.shutdownOutput()作为接受消息传递的标志,否则一直阻
                         *   塞起,shutdownOutput()只是关闭输出流,而不是关闭socket本身,将此方法放于一个流的末端,
                         *   表示已经传完东西了,无需再等候接受消息了
                         */
                        printWriter.println("发送给客户端"+socket1.getPort()+"的消息:"+ msg);

                    }
               }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
    public void start(){
        System.out.println("服务器启动....");
        try{
            poolExecutor.execute(new AcceptSocket());
        }catch (Exception e){
            e.printStackTrace();
        }
    }


    public static void main(String[] args) {
         new Server1().start();
    }

}

至于创建服务端socket部分代码放在构造器或者start()函数中都可以实现,至于客户端销毁后的处理,后续再考虑了,或者有大神帮忙也是可以的

四、结束语

其实来说,在通信这一块本人就是一个小白,此文仅在于记录学习过程,java线程池的使用也是属于入门级别,若有不当之处,望各位指教。第三部分所用的发送消息以及接受消息所用到的类与前面无异,故不再重复。文章也是写了就发布了,未曾严格审核其中错误

五、借鉴文章

1、 socket多线程编程.作者写确实好,本人写的手工创建多线程通信部分多来自于此

你可能感兴趣的:(JAVA利用Socket和ServerSocket实现一对多通信)