• Java 双工通信与websocket协议


    前言

    最近做项目,需要双工通信,考虑http协议,但是是单向的通信,只能请求响应,不能从服务端推送,如果要服务器推送,方式有很多,http轮训,长轮训,websocket等,实际上tcp传输层是双向通信的,原始的socket就可以实现。现在最常用的是websocket,因为可以复用http的底层tcp连接,方便,当然http3使用udp通信,基于QUIC保证连接可靠。

    Java socket方式

    TCP是面向双向连接的协议,刚好符合条件。server端暴露端口用于连接,实现多方client收发器

    1. package com.feng.socket.admin;
    2. import java.io.IOException;
    3. import java.net.ServerSocket;
    4. import java.net.Socket;
    5. import java.util.Map;
    6. import java.util.concurrent.ConcurrentHashMap;
    7. import java.util.concurrent.LinkedBlockingQueue;
    8. import java.util.concurrent.ThreadPoolExecutor;
    9. import java.util.concurrent.TimeUnit;
    10. import java.util.concurrent.atomic.AtomicInteger;
    11. public class SocketServer {
    12. private static AtomicInteger sessionId = new AtomicInteger(10000);
    13. private static Map<String, Socket> socketMap = new ConcurrentHashMap<>();
    14. private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(20));
    15. private ServerSocket serverSocket;
    16. public SocketServer(int port) {
    17. try {
    18. this.serverSocket = new ServerSocket(port);
    19. } catch (IOException e) {
    20. e.printStackTrace();
    21. }
    22. }
    23. public void start() {
    24. //server to client content
    25. threadPoolExecutor.execute(new WriteRunnable());
    26. try {
    27. while (true) {
    28. Socket socket = this.serverSocket.accept();
    29. String id = sessionId.incrementAndGet()+"";
    30. System.out.println("connect from client, the sessionId is :\t"+id);
    31. socketMap.put(id, socket);
    32. //client to server content
    33. threadPoolExecutor.execute(new ReadRunnable(id));
    34. }
    35. } catch (IOException e) {
    36. e.printStackTrace();
    37. }
    38. }
    39. public static Socket getSessionSocket(String sessionId){
    40. return socketMap.get(sessionId);
    41. }
    42. public static void removeSessionSocket(String sessionId){
    43. socketMap.remove(sessionId);
    44. }
    45. public static void main(String[] args) {
    46. new SocketServer(8083).start();
    47. }
    48. }

    其中收的线程

    1. package com.feng.socket.admin;
    2. import java.io.*;
    3. import java.net.Socket;
    4. public class ReadRunnable implements Runnable{
    5. private String sessionId;
    6. public ReadRunnable(String sessionId) {
    7. this.sessionId = sessionId;
    8. }
    9. @Override
    10. public void run() {
    11. try {
    12. Socket socket = SocketServer.getSessionSocket(sessionId);
    13. if (socket.isClosed()) {
    14. System.out.println("client已断开连接------");
    15. return;
    16. }
    17. InputStream in = socket.getInputStream();
    18. Reader reader = new InputStreamReader(in);
    19. BufferedReader br = new BufferedReader(reader);
    20. while (true) {
    21. if (socket.isClosed()) {
    22. SocketServer.removeSessionSocket(sessionId);
    23. System.out.println("client已断开连接------");
    24. break;
    25. }
    26. try {
    27. String line = br.readLine();
    28. if (line == null) {
    29. System.out.println("client已断开连接------");
    30. break;
    31. }
    32. System.out.println(sessionId + ":" + line);
    33. if ("shutdown".equalsIgnoreCase(line)) {
    34. socket.close();
    35. SocketServer.removeSessionSocket(sessionId);
    36. System.out.println("client已断开连接------");
    37. break;
    38. }
    39. } catch (IOException e) {
    40. e.printStackTrace();
    41. }
    42. }
    43. br.close();
    44. reader.close();
    45. in.close();
    46. socket.close();
    47. } catch (IOException e) {
    48. throw new RuntimeException(e);
    49. }
    50. }
    51. }

     发的方式,使用console输入

    1. package com.feng.socket.admin;
    2. import java.io.*;
    3. import java.net.Socket;
    4. import java.util.Map;
    5. import java.util.concurrent.ConcurrentHashMap;
    6. public class WriteRunnable implements Runnable{
    7. Map<String, PrintWriter> cache = new ConcurrentHashMap<>();
    8. @Override
    9. public void run() {
    10. String line = "";
    11. PrintWriter pw;
    12. try (InputStreamReader inputStreamReader = new InputStreamReader(System.in);
    13. BufferedReader br = new BufferedReader(inputStreamReader)) {
    14. while (true) {
    15. if ((line = br.readLine()) != null) {
    16. // 使用 sessionId:xxx 表示发送某个client,发送所有client广播就是迭代发送
    17. String sessionId = line.substring(0,5);
    18. String data = line.substring(6);
    19. Socket socket = SocketServer.getSessionSocket(sessionId);
    20. if (socket.isClosed()) {
    21. System.out.println("server连接已断开-----");
    22. }
    23. if (cache.containsKey(sessionId)) {
    24. pw = cache.get(sessionId);
    25. } else {
    26. try {
    27. OutputStream out = socket.getOutputStream();
    28. pw = new PrintWriter(out);
    29. cache.put(sessionId, pw);
    30. } catch (IOException e) {
    31. throw new RuntimeException(e);
    32. }
    33. }
    34. pw.println(data);
    35. pw.flush();
    36. if ("shutdown".equalsIgnoreCase(data)) {
    37. try {
    38. cache.get(sessionId).close();
    39. socket.close();
    40. } catch (IOException e) {
    41. e.printStackTrace();
    42. }
    43. }
    44. }
    45. }
    46. } catch (IOException e) {
    47. throw new RuntimeException(e);
    48. }
    49. }
    50. }

    client端代码

    1. package com.feng.socket.client;
    2. import java.io.*;
    3. import java.net.Socket;
    4. import java.util.Scanner;
    5. public class SocketClient {
    6. public static void main(String[] args) throws IOException {
    7. final Socket socket = new Socket("192.168.79.104", 8083);
    8. final Scanner scanner = new Scanner(System.in);
    9. new Thread(new Runnable() {
    10. @Override
    11. public void run() {
    12. String line = "";
    13. try (InputStream in = socket.getInputStream();
    14. Reader reader = new InputStreamReader(in);
    15. BufferedReader br = new BufferedReader(reader)) {
    16. while (true) {
    17. if (socket.isClosed()) {
    18. System.out.println("server断开连接==========");
    19. break;
    20. }
    21. line = br.readLine();
    22. if (line != null) {
    23. System.out.println(line);
    24. if ("shutdown".equalsIgnoreCase(line)) {
    25. break;
    26. }
    27. } else {
    28. System.out.println("server断开连接==========");
    29. scanner.close();
    30. break;
    31. }
    32. }
    33. } catch (IOException e) {
    34. throw new RuntimeException(e);
    35. } finally {
    36. try {
    37. socket.close();
    38. System.exit(0);
    39. } catch (IOException e) {
    40. e.printStackTrace();
    41. }
    42. }
    43. }
    44. }).start();
    45. new Thread(new Runnable() {
    46. @Override
    47. public void run() {
    48. String line;
    49. try (OutputStream out = socket.getOutputStream();
    50. PrintWriter pw = new PrintWriter(out)) {
    51. while (true) {
    52. if (socket.isClosed()) {
    53. System.out.println("client断开连接==========");
    54. break;
    55. }
    56. if (scanner.hasNext()) {
    57. line = scanner.next();
    58. pw.println(line);
    59. pw.flush();
    60. out.flush();
    61. if ("shutdown".equalsIgnoreCase(line)) {
    62. break;
    63. }
    64. }
    65. }
    66. if (!socket.isClosed()) {
    67. socket.close();
    68. }
    69. } catch (IOException e) {
    70. throw new RuntimeException(e);
    71. }
    72. }
    73. }).start();
    74. }
    75. }

     相对比较简单,测试如下

    实现了双工,简单模式BIO。

    websocket协议

    实际上websocket就是用来解决这个问题的一个标准,来自于Oracle官网JSR 356, Java API for WebSocket (oracle.com)

     目前的成熟方案是在javaee上实现协议升级,复用Tomcat等的底层容器的TCP连接。spring boot demo,配置bean,当然也可以使用Oracle官网的方式,这里springboot封装了。

    1. package com.feng.socket.admin;
    2. import org.springframework.context.annotation.Bean;
    3. import org.springframework.context.annotation.Configuration;
    4. import org.springframework.web.socket.server.standard.ServerEndpointExporter;
    5. @Configuration
    6. public class WebSocketConfig {
    7. @Bean
    8. public ServerEndpointExporter initServerEndpointExporter(){
    9. return new ServerEndpointExporter();
    10. }
    11. }

    写一个server端websocket连接收发器

    1. package com.feng.socket.admin;
    2. import org.slf4j.Logger;
    3. import org.slf4j.LoggerFactory;
    4. import org.springframework.stereotype.Component;
    5. import javax.websocket.*;
    6. import javax.websocket.server.PathParam;
    7. import javax.websocket.server.ServerEndpoint;
    8. import java.io.IOException;
    9. import java.util.Map;
    10. import java.util.concurrent.ConcurrentHashMap;
    11. @Component
    12. @ServerEndpoint("/websocket/server/{sessionId}")
    13. public class SocketServer {
    14. private static final Logger LOGGER = LoggerFactory.getLogger(SocketServer.class);
    15. private static Map<String, Session> sessionMap = new ConcurrentHashMap<>();
    16. private String sessionId = "";
    17. @OnOpen
    18. public void onOpen(Session session, @PathParam("sessionId") String sessionId) {
    19. this.sessionId = sessionId;
    20. sessionMap.put(sessionId, session);
    21. LOGGER.info("new connect, sessionId is " + sessionId);
    22. }
    23. @OnClose
    24. public void onClose() {
    25. sessionMap.remove(sessionId);
    26. LOGGER.info("close socket, the sessionId is " + sessionId);
    27. }
    28. @OnMessage
    29. public void onMessage(String message, Session session) {
    30. LOGGER.info("--------- receive message: " + message);
    31. }
    32. @OnError
    33. public void onError(Session session, Throwable error) {
    34. LOGGER.error(error.getMessage(), error);
    35. }
    36. public static void sendMessage(Session session, String message) throws IOException {
    37. session.getBasicRemote().sendText(message);
    38. }
    39. public static Session getSession(String sessionId){
    40. return sessionMap.get(sessionId);
    41. }
    42. }

    写一个发送接口

    1. package com.feng.socket.admin;
    2. import org.springframework.boot.SpringApplication;
    3. import org.springframework.boot.autoconfigure.SpringBootApplication;
    4. import org.springframework.web.bind.annotation.RequestMapping;
    5. import org.springframework.web.bind.annotation.RestController;
    6. import javax.websocket.Session;
    7. import java.io.IOException;
    8. @RestController
    9. @SpringBootApplication
    10. public class SocketMain {
    11. public static void main(String[] args) {
    12. SpringApplication.run(SocketMain.class, args);
    13. }
    14. @RequestMapping("/msg")
    15. public String sendMsg(String sessionId, String msg) throws IOException {
    16. Session session = SocketServer.getSession(sessionId);
    17. SocketServer.sendMessage(session, msg);
    18. return "send " + sessionId + " : " + msg;
    19. }
    20. }

    在使用java-websocket写一个client端

    1. <dependency>
    2. <groupId>org.java-websocket</groupId>
    3. <artifactId>Java-WebSocket</artifactId>
    4. <version>1.5.3</version>
    5. </dependency>

    code

    1. package com.feng.socket.client;
    2. import org.java_websocket.client.WebSocketClient;
    3. import org.java_websocket.enums.ReadyState;
    4. import org.java_websocket.handshake.ServerHandshake;
    5. import java.net.URI;
    6. import java.net.URISyntaxException;
    7. import java.util.Scanner;
    8. public class SocketClient {
    9. public static void main(String[] args) throws URISyntaxException {
    10. WebSocketClient webSocketClient = new WebSocketClient(new URI("ws://127.0.0.1:8083/websocket/server/10001")) {
    11. @Override
    12. public void onOpen(ServerHandshake serverHandshake) {
    13. System.out.println(serverHandshake.getHttpStatus() + " : " + serverHandshake.getHttpStatusMessage());
    14. }
    15. @Override
    16. public void onMessage(String s) {
    17. System.out.println("receive msg is " + s);
    18. }
    19. @Override
    20. public void onClose(int i, String s, boolean b) {
    21. System.out.println(s);
    22. }
    23. @Override
    24. public void onError(Exception e) {
    25. e.printStackTrace();
    26. }
    27. };
    28. webSocketClient.connect();
    29. while (!ReadyState.OPEN.equals(webSocketClient.getReadyState())) {
    30. System.out.println("wait for connecting ...");
    31. }
    32. webSocketClient.send("hello");
    33. Scanner scanner = new Scanner(System.in);
    34. while (scanner.hasNext()) {
    35. String line = scanner.next();
    36. webSocketClient.send(line);
    37. }
    38. System.out.println("start websocket client...");
    39. }
    40. }

    实现服务端与client端双向发送,点对点传输。 

    websocket协议原理

    通过抓包,发现websocket实际上是http协议上的升级

     先client通过http协议发送协商,connection :Upgrade ,Upgrade的模式是websocket,按照这个理论,我们也可以自定义一种其他Upgrade的协议传输数据。

    协商的key和版本

    然后服务端返回协商的结果

     

    协议达成,如果需要TLS加密传输还要证书传递等

     后面就会ping pong

     

    总结

    这里的双向通信,使用的TCP的特性,websocket也是在http的基础上升级而来,实际上现在的QUIC协议也可以实现在UDP上通信,就是HTTP3.0,就不再适合websocket了,估计会出现新的成熟技术。

  • 相关阅读:
    人人皆知的人工智能真的稳定吗?它的发展前景如何?
    java毕业生设计校园闲置物品交换平台系统计算机源码+系统+mysql+调试部署+lw
    苹果手机照片如何导入电脑?无损快速的传输办法分享!
    通过创建自定义标签来扩展HTML
    EL表达式 Jstl (附上代码理解)
    minikube 快速使用入门 - pod - 外传
    2022年2000元能玩原神的手机推荐 这3款值得买
    react--redux
    英伟达两个最新元宇宙布局
    如何基于YAML设计接口自动化测试框架?看完秒会!
  • 原文地址:https://blog.csdn.net/fenglllle/article/details/125462222