欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java实现客户端与多线程服务器的通信(网络编程)

程序员文章站 2022-06-06 09:36:15
...

1.多线程服务器端原理分析

  1. 一般情况下同一时刻服务器都不止和一个客户端进行通信,如果服务器只有一个线程,那么在多任务操作时任务之间就需要等待,因此需要为服务器创建多条线程提供给不同的同时客户端使用。
  2. 为服务器创建多线程,就类似于为服务器创建==“影分身”==,让每一条线程都能享有一个独立的"影分身"服务器。
  3. 为了满足上述要求,每一条线程都必须传入一个新创建的Runnable接口实现类,实现类中的run()方法运行服务器的主程序。

2.客户端代码

客户端主要功能:

  1. 客户端向服务器发送信息。
  2. 客户端接收服务器的反馈信息。
代码如下
/*实现TCP通信的客户端程序
  实现步骤:
        1.创建Socket对象 (主动连接服务器)
            Socket(String host, int port)  host为服务器的地址(此处服务器也在本机所以使用环回地址),port为服务器中应用的端口
            
        2.OutputStream getOutputStream()
            返回套接字中的字节输出流,此时就可以使用write()方法写入数据,写入服务器
            
        3.InputStream getInputStream()
            返回套接字中的字节输入流对象,调用read()方法可以读取服务器发来的数据
            
        4.释放资源 close()
 */
 public class TCPClient {

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

        Socket client = new Socket("127.0.0.1",9000);

        OutputStream out = client.getOutputStream();

        byte[] bytes = new byte[1024];
        out.write("请求连接服务器".getBytes());  //没有写在文件中,写入了服务器(发送给了服务器)



        InputStream in = client.getInputStream();

        byte[] inBytes = new byte[1024];
        int inLen = in.read(inBytes);

        String s = new String(inBytes, 0, inLen);
        System.out.println(s);

        client.close();
    }

}

3.服务器代码

服务器主要功能

  1. 服务器需要接收客户端发来的消息。
  2. 接收到消息后,服务器需要向客户端发送反馈信息。
    由于需要为服务器创建多线程,因此服务器实现的程序需要放在Runnable实现类的run()方法中
代码如下
/*实现TCP通信中的服务器程序
  实现TCP服务器步骤:
      1.创建ServerSocket对象
        ServerSocket(in port) 端口号

      2.等待客户端的连接,如果没有客户端连接,永远等待
        ServerSocket类方法 accept()  (等待客户端的连接)
        accept() 方法的返回值为Socket对象(客户端套接字,包含客户端的IP地址,用于回复信息)

      3.Socket对象中获取字节输入流
        InputStream getInputStream()
        对象调用read()方法,读取客户端发来的数据

      4.Socket对象中获取字节输出流
      OutputStream getOutputStream()
        对象调用write()方法,向客户端写入(回复)数据

      5.释放资源  close()

 */
public class TCPThreadServerDemo {

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

        ServerSocket server = new ServerSocket(9000);
        
		//循环放在此处是为了,当每次客户端与服务器通信完成时,服务器不停止运行,而是又再一次进入侦听状态,侦听是否还有服务器向自己发送信息;
        while (true) {
            Socket accept = server.accept();

            Thread thread = new Thread(new ThreadServer(accept));

            thread.start();

        }
    }
}

public class ThreadServer implements Runnable{

    private Socket accept;
	//测试类中传入服务器的侦听accept()侦听到的客户端对象
    public ThreadServer(Socket accept){
        this.accept = accept;
    }

    @Override
    public void run() {

            try {
                Server.ServerMethod(accept);
            } catch (IOException e) {
                e.printStackTrace();
            }
    }
}

public class Server {

    public static void ServerMethod(Socket accept) throws IOException {

        InputStream in = accept.getInputStream();

        byte[] bytes = new byte[1024];
        int len = in.read(bytes);
        String s = new String(bytes, 0, len);
        System.out.println(s + Thread.currentThread().getName());


        OutputStream out = accept.getOutputStream();
        out.write("连接成功!".getBytes());

        accept.close();

    }
}

测试使用了6个客户端与服务器进行通信,使用这种方式模拟多线程服务器的通信,服务器端结果如下:

请求连接服务器Thread-0
请求连接服务器Thread-1
请求连接服务器Thread-2
请求连接服务器Thread-3
请求连接服务器Thread-4
请求连接服务器Thread-5

4.分析结果

对上述结果进行分析,发现被使用过的线程任务执行完毕后就死亡了,不能被再次使用,下一个客户端对服务器发起连接时将使用新new出来的Thread,旧的线程资源其实已经使用完了但是无法再次调用start()使用,这要会导致资源浪费;因此我对代码进行了一些修改,同时开启多路线程,使得每个线程内部的run()方法套上while(true)循环使得线程永不终止,将服务器的侦听器放到线程的内部,测试类只需向线程代码传递服务器的连接(套接字)对象即可;当客户端向服务器发送数据时选择进入一个线程,完成线程任务即可。

5.修改版本

代码如下

代码只修改服务器端多线程入口部分,客户端和服务器端主题代码与上述一致,此处不重复书写

public class TCPThreadServerDemo {

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

        ServerSocket server = new ServerSocket(9000);

		//创建多条线程,每条线程都会独立的侦听客户端发送来的消息;
        Thread thread0 = new Thread(new ThreadServer(server));

        Thread thread1 = new Thread(new ThreadServer(server));

        Thread thread2 = new Thread(new ThreadServer(server));

        thread0.start();
        thread1.start();
        thread2.start();

    }
}
public class ThreadServer implements Runnable{

    private ServerSocket server;

    public ThreadServer(ServerSocket server){
        this.server = server;
    }


    @Override
    public void run() {

        while (true){

            try {
            //将侦听方法写在了run()方法内,这样每条线程都可以侦听+执行服务器命令,并且都可以反复侦听,而不是像上面的方法,侦听到客户端消息后再创建新线程;
                Socket accept = server.accept();
                Server.ServerMethod(accept);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

这样线程可以被复用,不像上面的方法每条线程只能执行一次通信任务;但是与此同时也出现了问题,如果现在没有客户端向服务器发送请求,但是三个线程还是同时开启着并且不断侦听,十分浪费,如何做到多线程时线程可以复用,同时又能做到无任务时线程可以关闭,侦听到任务时线程才开启。此时就需要使用线程池,当需要使用线程时,向线程池获取线程,此线程就被**,试用结束后归还线程(线程不会死亡),线程进入空闲状态;这样使用后的线程可以被复用,同时又不会在无任务状态下使多个线程持续运行,解决了上述两种服务器端多线程通信的两个矛盾点;

6.采用线程池的多线程服务器端

代码如下

省略上面已有的客户端和服务器端主程序
这展示被修改部分的代码

public class TCPThreadServerDemo {

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

        //创建线程池对象,线程池线程容量为3
        ExecutorService service = Executors.newFixedThreadPool(3);

        ServerSocket server = new ServerSocket(9000);


        while (true) {
            Socket accept = server.accept();

//            Thread thread = new Thread(new ThreadServer(accept));
			//提交线程任务,从线程池中获取线程
            service.submit(new ThreadServer(accept));
            //System.out.println(service);

        }
    }
}
public class ThreadServer implements Runnable{

    private Socket accept;

    public ThreadServer(Socket accept){
        this.accept = accept;
    }

    @Override
    public void run() {

            try {
                Server.ServerMethod(accept);
            } catch (IOException e) {
                e.printStackTrace();
            }
    }
}

测试使用了7个客户端与服务器进行通信,服务器端结果如下:

请求连接服务器pool-1-thread-1
请求连接服务器pool-1-thread-2
请求连接服务器pool-1-thread-3
请求连接服务器pool-1-thread-1
请求连接服务器pool-1-thread-2
请求连接服务器pool-1-thread-3
请求连接服务器pool-1-thread-1

上述是初学者的个人想法,如果有任何问题或不同观点,或者我写的有错,都可以提出,大家一起讨论,不过请轻点喷 ^ - ^ !