JAVA使用Socket进行异步通讯

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/liuajiehe1234567/article/details/79173196

本文章除了基本的socket知识以外,还用到了包括死循环获取用户连接请求,异步多线程的消息读取和写入等知识,博主前后在踩坑上花的时间至少都有10个小时,希望能帮助到你

从第一次接触socket到现在已经经过了小半年的时间,当时老师只是提了一下socket通讯是长连接,一个socket可以互相发送多次消息,但是具体实现大家都没有做出来,复制了网上的代码进行实现也不知道原理是什么,今天终于又花了将近5个小时将原理弄明白搞透彻,并且实现了异步的socket通信。

先看一下单线程的通信,这里是一段菜鸟教程上的socket单线程通信的代码

单线程的socket通信实例

服务器端:

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
 
public class Server {
   public static void main(String[] args) {
      try {
         ServerSocket ss = new ServerSocket(8888);
         System.out.println("启动服务器....");
         Socket s = ss.accept();
         System.out.println("客户端:"+s.getInetAddress().getLocalHost()+"已连接到服务器");
         
         BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
         //读取客户端发送来的消息
         String mess = br.readLine();
         System.out.println("客户端:"+mess);
         BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
         bw.write(mess+"\n");
         bw.flush();
      } catch (IOException e) {
         e.printStackTrace();
      }
   }
}
客户端:

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.net.UnknownHostException;
 
public class Client {
   public static void main(String[] args) {
      try {
         Socket s = new Socket("127.0.0.1",8888);
         
         //构建IO
         InputStream is = s.getInputStream();
         OutputStream os = s.getOutputStream();
         
         BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));
         //向服务器端发送一条消息
         bw.write("测试客户端和服务器通信,服务器接收到消息返回到客户端\n");
         bw.flush();
         
         //读取服务器返回的消息
         BufferedReader br = new BufferedReader(new InputStreamReader(is));
         String mess = br.readLine();
         System.out.println("服务器:"+mess);
      } catch (UnknownHostException e) {
         e.printStackTrace();
      } catch (IOException e) {
         e.printStackTrace();
      }
   }
}
代码运行效果如下

服务器端控制台:


客户端控制台:



现在从计算机网络的角度来解析这段代码里的通信流程:

一、JAVA底层封装了TCP通信协议接口的实现,当new ServerSocket(portNumber)时,就将当前主机的8888端口打开了,这个时候客户端其实就可以发起请求,不过此时发起的请求归操作系统管,java并不会对其进行处理,而是将当前的连接请求放到请求队列中(请求队列的长度和超时时间都可以配置),直到当前的serverSocket调用accept()方法才会从队列中取出一个最先加入的请求进行处理

二、客户端和服务器端互相使用流来发送消息,这里使用的发送消息的流是带缓冲区的BufferedWriter,字符输出流。而写入结束,进行发送的标志是调用flush()方法,这是一个很有意思过程,我们知道flush的作用是将当前缓冲区中的内容输出到目的地,如果写入的大小超过了缓冲区则自动进行flush。那么如果使用不带缓冲区的流是不是就直接输出到指定目标呢?这里暂不做研究,以后有空可以试试。

三、在发送消息之后,另外一端通过当前的BufferedReader的readline方法读取到换行符获取字符串,接着输出到控制台,这个过程如果没有读取到换行符则此方法会一直堵塞代码。

四、服务器端代码执行结束,服务器关闭(和severSocket的close()方法效果相同)

这个流程其实有一些超出我们常理认知的地方,总结出来大概是两点疑问

1.怎么你这个服务器说关就关呢?我看别人的服务器都是一直开着处理请求的。

2.一个socket只能发送一次信息吗?socket不应该是TCP协议支持的全双工长连接吗?怎么发完一次就没有了,一个socket可以发多次吗?

关于第一点很好处理,我们只需要使用循环来控制当前服务器的accept方法,反复来获取新的socket连接即可,注意这里是“获取新的socket”连接,新获取的socket和之前的socket没有关系,这一步只意味着服务器可以一直对8888端口进行监听,可以随时拿到新请求socket,让服务器可以继续对新的连接提供服务,如果没有新的请求加入,accept()方法一直阻塞

如下操作:

while(true){

Socket client =serverSocket.accept();

.......

}

第二点的问题也就是处理问题的核心了

  socket确实是tcp支持的全双工长连接,如果只是让客户端反复的new socket()来请求,然后每次在服务器端使用循环不停的accept来获取客户端请求,然后每次只交换一次数据,当然可以做到即时的通讯,但是这样开销是极大的,并且服务器端无法对客户的访问进行持久化处理,简单的说,这就像HTTP协议,只能进行短连接,彼此互相收发一次数据就拜拜了,并不是长连接

那么如何做到一个socket收发多次数据呢?

    在以上的服务器端代码的尾部添加一行

 bw.write("第二次发送数据\n"); bw.flush();

 

  注意一定要在字符串末尾添加换行符,否则代码会一直堵塞

在客户端代码的尾部添加:

 System.out.println(br.readline());

运行结果:

服务器端输出结果和之前相同

客户端输出:

可以看到,一个socket是可以进行多次输出数据的,只要控制代码的运行流程即可(比如需要防止readline()的阻塞)

异步的socket请求处理

将原本的菜鸟教程上的代码稍作修改如下

服务器端:

package com.testsocket.server;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
 
public class Server {
   public static void main(String[] args) {
	   ServerSocket ss = null;
	try {
		ss = new ServerSocket(8888);
		 System.out.println("启动服务器!");
	} catch (IOException e1) {
		// TODO Auto-generated catch block
		e1.printStackTrace();
	}
	   while(true){
      try {
         Socket s = ss.accept();
         System.out.println("客户端:"+s.getInetAddress().getLocalHost()+"已连接到服务器");
         BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
         //读取客户端发送来的消息
         BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
         bw.write("你已和服务器建立通信,现在进入你的请求处理线程"+"\n");
         bw.flush();
         Scanner sc  = new Scanner(System.in);
         Thread readThread = new Thread(){
             public void run(){
                 while(true){
                     
                     try {
                    	 String msg = br.readLine();
                         System.out.println(msg);
                     } catch (IOException e) {
                         e.printStackTrace();
                     }
                     
                 }
             }
           };
           Thread writeThread = new Thread(){
               public void run(){
                   while(true){
                       try {
                           bw.write(sc.next()+"\n");
                           bw.flush();
                       } catch (IOException e) {
                           e.printStackTrace();
                       }
                   }
               }
           };
           readThread.start();
           writeThread.start();
      } catch (IOException e) {
         e.printStackTrace();
      }
	   }
   }
}
客户端:

package com.testsocket;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;

public class Client {
    public static void main(String[] args) {
        try {
            Socket s = new Socket("127.0.0.1",8888);

            //构建IO
            InputStream is = s.getInputStream();
            OutputStream os = s.getOutputStream();
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            Scanner sc = new Scanner(System.in);
            Thread readThread = new Thread(){
              public void run(){
                  while(true){
                      String msg = null;
                      try {
                          msg = br.readLine();
                      } catch (IOException e) {
                          e.printStackTrace();
                      }
                      System.out.println(msg);
                  }
              }
            };
            Thread writeThread = new Thread(){
                public void run(){
                    while(true){
                        try {
                            bw.write(sc.next()+"\n");
                            bw.flush();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            };
            readThread.start();
            writeThread.start();
        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

运行实例:


精分现场23333

我们把客户端的代码再复制一份,让两个客户端进行连接

服务器运行实例




socket是一门很深的技术,像这里使用原生的socket光是连接了两个客户端就让其中一个客户端出现了服务器响应接收不到的情况,具体和sleep时间,封包等都有关,不做深究

虽然代码是密密麻麻乱了点,但是首先要知道,在服务器端为每个客户端配置两个线程是必须的,一个用来接收请求,一个用来发送请求,用户每一次请求后就和这两个线程进行绑定,然后再在线程里对客户进行业务操作,服务器端则在主线程中继续拿取下一个请求,再分配两个线程,再进行客户端绑定。换句话说,异步处理不是只一种socket处理的方式,它是socket会话处理中最好的方式。

总结一下,java对socket的处理机制要求我们必须使用两个方法来处理socket连接

1.使用死循环来循环获取socket连接,两个好处:可以不断获取新的连接并且可以防止服务器代码运行到尾部down掉

2.使用多线程来处理当前客户端请求的业务,服务器很忙,没空一步一步的跟你把代码执行下去,所以每一个客户端都需要绑定一个输出线程和一个输入线程对它的业务进行处理

猜你喜欢

转载自blog.csdn.net/liuajiehe1234567/article/details/79173196