1. 使用 Socket ServerSocket 进行 BIO 模型的演示,ServerSocket 代表的是**在服务器端的Socket**,而 Socket 代表的是**客户端的Socket**<br />![](https://cdn.nlark.com/yuque/0/2022/png/21405095/1641461703843-c8d31e4c-2481-459a-9015-bafdbffb92cf.png#clientId=u06a70c8a-6300-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=359&id=u4c46567b&margin=%5Bobject%20Object%5D&originHeight=543&originWidth=764&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=stroke&taskId=uc147f772-0804-4a38-a84c-53fb80b4e58&title=&width=505)<br />**BIO模型演示**

BIO步骤:

服务器创建 ServerSocket 对象的时候需要提供端口进行绑定(bind),实现绑定之后,ServerSocket 可以作为其他客户端与服务器进行通信时使用的端点,服务器端会一直监听的绑定端口,任何客户端应用只要把想要发送的服务器的消息发送服务器到绑定的端口,服务器就可以收到并进行处理
服务器完成绑定之后,会调用 accept 函数,该函数是一个阻塞式的调用,在没有客户端建立连接的时候,accept 函数会一直阻塞当前线程,直到客户端创建了 Socket 对象与服务器进程进行连接( 在生成客户端 Socket 时需要传入服务器所在的主机、监听端口,有这些信息才能实现与服务器 ServerSocket 的连接);一旦服务器的 accept 函数接收了客户端发起的连接将会返回一个 Socket 对象,该Socket对象可以和客户端的 Socket 进行通信
完成连接后,通过两边的端点使用 IO流 进行信息传输,两边信息交换完成后可以使用 close 函数关闭客户端的 Socket 对象与服务器端的 ServerSocket 对象


代码演示:

源码下载:

  1. https://gitee.com/dmbjzorg/Netty

服务器端:

  1. package study;
  2. import java.io.*;
  3. import java.net.ServerSocket;
  4. import java.net.Socket;
  5. /* 服务器端 */
  6. public class MyServer {
  7. private static final int port = 8888;
  8. private static final String exit = "exit";
  9. public static void main(String[] args) {
  10. /* 创建ServerSocket对象并绑定端口 */
  11. ServerSocket serverSocket = null;
  12. try {
  13. serverSocket = new ServerSocket(port);
  14. System.out.println("服务器端创建serverSocket对象,开始监听"+port+"端口");
  15. while (true){
  16. Socket socket = serverSocket.accept(); //等待客户端连接,获取到连接后与客户端的Socket进行通信
  17. System.out.println("服务器端收到端口为"+socket.getPort()+"客户端的Socket请求");
  18. /* 从客户端Socket读取数据*/
  19. InputStreamReader inputStream = new InputStreamReader(socket.getInputStream());
  20. BufferedReader reader = new BufferedReader(inputStream);
  21. OutputStreamWriter outputStream = new OutputStreamWriter(socket.getOutputStream());
  22. BufferedWriter writer = new BufferedWriter(outputStream);
  23. /* 修改收到的数据返还给服务器端 */
  24. String lineMsg = null;
  25. while ( (lineMsg = reader.readLine())!=null ) {
  26. System.out.println("服务器端收到端口为"+socket.getPort()+"客户端的消息:"+lineMsg);
  27. writer.write("服务器:"+lineMsg+"\n");
  28. writer.flush();
  29. /* 退出服务器端 */
  30. if(exit.equals(lineMsg)){
  31. System.out.println("端口为"+socket.getPort()+"的客户端断开连接");
  32. break;
  33. }
  34. }
  35. }
  36. } catch (IOException e) {
  37. e.printStackTrace();
  38. }finally {
  39. if(serverSocket!=null){
  40. try {
  41. serverSocket.close();
  42. } catch (IOException e) {
  43. e.printStackTrace();
  44. }
  45. System.out.println("服务端ServerSocket已关闭");
  46. }
  47. }
  48. }
  49. }

客户端:

  1. package study;
  2. import java.io.*;
  3. import java.net.Socket;
  4. /* 客户端请求 */
  5. public class Browser {
  6. private static final String address = "127.0.0.1";
  7. private static final int port = 8888;
  8. private static final String exit = "exit";
  9. public static void main(String[] args) {
  10. Socket socket = null;
  11. BufferedWriter writer = null;
  12. try {
  13. socket = new Socket(address,port);
  14. /* 创建IO流 */
  15. BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
  16. writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
  17. /* 控制台输入信息 */
  18. BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in));
  19. while (true){
  20. String lineStr = consoleReader.readLine();
  21. /* 发送消息给服务器 */
  22. writer.write(lineStr+"\n");
  23. writer.flush();
  24. /* 读取服务器返回的消息 */
  25. String msg = reader.readLine();
  26. System.out.println("服务器返回的消息: "+msg);
  27. /* 查看用户是否退出 */
  28. if(exit.equals(lineStr)){
  29. break;
  30. }
  31. }
  32. } catch (IOException e) {
  33. e.printStackTrace();
  34. }finally {
  35. /* 关闭 writer的时候会自动清空内部缓存区且关闭 Socket */
  36. if(writer!=null){
  37. try {
  38. writer.close();
  39. } catch (IOException e) {
  40. e.printStackTrace();
  41. }
  42. System.out.println("客户端Socket已关闭");
  43. }
  44. }
  45. }
  46. }

测试:

运行 MyServer 先创建出 ServerSocket 对象,再运行 Browser 后 MyServer 获取到连接请求,使用IO流传输消息后收到指定信息进行退出操作
image.png
测试案例


聊天室案例:

用户加入到聊天室后可以看到后续每个成员的发言
绘图1.png
BIO聊天室案例时序图

聊天室服务器端:

使用线程池对资源进行限制,由于核心数为10,因此包括服务器在内最多有9个用户可以在聊天室中进行正常交流,9位在线用户不会收到未加入到聊天室用户发送的消息

  1. package chatroom;
  2. import java.io.BufferedWriter;
  3. import java.io.IOException;
  4. import java.io.OutputStreamWriter;
  5. import java.io.Writer;
  6. import java.net.ServerSocket;
  7. import java.net.Socket;
  8. import java.util.HashMap;
  9. import java.util.Map;
  10. import java.util.concurrent.ArrayBlockingQueue;
  11. import java.util.concurrent.ThreadPoolExecutor;
  12. import java.util.concurrent.TimeUnit;
  13. /* 聊天室服务器端 */
  14. public class ChatServer {
  15. private static int DEFAULT_PORT = 8888; //监听端口号
  16. private static final String QUIT = "quit"; //用户输入quit时退出聊天
  17. private ServerSocket serverSocket; //接收客户端的Socket
  18. private ThreadPoolExecutor executorService; //线程池
  19. private Map<Integer, Writer> connectedClients; //保存目前为止所有客户发送的消息,key:客户端的端口号 value:服务端向对应的客户端发送信息时的 writer
  20. /* 初始化参数 */
  21. public ChatServer() {
  22. executorService = new ThreadPoolExecutor(5,10,200, TimeUnit.MILLISECONDS,new ArrayBlockingQueue<>(10));
  23. connectedClients = new HashMap<>();
  24. }
  25. /* 新客户端用户加入到聊天室服务器 */
  26. public synchronized void addClient(Socket socket) throws IOException {
  27. if (socket!=null){
  28. int port = socket.getPort();
  29. BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
  30. connectedClients.put(port,writer);
  31. System.out.println("客户端["+port+"]已经连接到服务器");
  32. }
  33. }
  34. /* 客户端用户退出聊天室服务器 */
  35. public synchronized void removeClient(Socket socket) throws IOException {
  36. if (socket!=null){
  37. int port = socket.getPort();
  38. //先判断当前客户端有没有在map中(之前有没有与客户端建立连接)
  39. if (connectedClients.containsKey(port)){
  40. //如果在,直接将writer流关闭
  41. connectedClients.get(port).close();
  42. }
  43. connectedClients.remove(port); //将对应的值从map中去除
  44. System.out.println("客户端["+port+"]断开连接");
  45. }
  46. }
  47. /* 接收到某一客户发送的消息后将其发送给其他用户
  48. * @param socket 发送消息的用户
  49. * @param fwdMsg 客户端需要转发的消息
  50. * */
  51. public synchronized void forwardMessage(Socket socket,String fwdMsg){
  52. connectedClients.forEach((k,v)->{
  53. /* 如果该端口与发送消息的用户端口号不一致,则转发消息 */
  54. if (!k.equals(socket.getPort())){
  55. Writer writer = connectedClients.get(k);
  56. //将消息转发给客户
  57. try {
  58. writer.write(fwdMsg);
  59. writer.flush();
  60. } catch (IOException e) {
  61. e.printStackTrace();
  62. }
  63. }
  64. });
  65. }
  66. /* 启动服务器端 */
  67. public void start(){
  68. try {
  69. serverSocket = new ServerSocket(DEFAULT_PORT); //绑定监听端口
  70. System.out.println("服务器已启动,监听端口:"+DEFAULT_PORT+"......");
  71. while (true){
  72. //等待是否有客户端需要尝试建立连接
  73. Socket socket = serverSocket.accept();
  74. //创建ChatHandler线程
  75. executorService.execute(new ChatHandler(this,socket));
  76. }
  77. } catch (IOException e) {
  78. e.printStackTrace();
  79. } finally {
  80. if (serverSocket!=null){
  81. try {
  82. serverSocket.close();
  83. System.out.println("关闭ServerSocket");
  84. } catch (IOException e) {
  85. e.printStackTrace();
  86. }
  87. }
  88. }
  89. }
  90. /* 检查用户是否退出 */
  91. public boolean readyToQuit(String msg){
  92. return QUIT.equals(msg)?true:false;
  93. }
  94. public static void main(String[] args) {
  95. ChatServer server = new ChatServer();
  96. server.start();
  97. }
  98. }

聊天室建立对应关系方法:

  1. package chatroom;
  2. import java.io.BufferedReader;
  3. import java.io.IOException;
  4. import java.io.InputStreamReader;
  5. import java.net.Socket;
  6. /* 用于建立客户端与用户一对一的交换(一个用户对应一个线程) */
  7. public class ChatHandler implements Runnable{
  8. private ChatServer server; //用操作存放在服务器端的map集合
  9. private Socket socket; //建立连接的客户端socket
  10. public ChatHandler(ChatServer server, Socket socket) {
  11. this.server = server;
  12. this.socket = socket;
  13. }
  14. @Override
  15. public void run() {
  16. try {
  17. server.addClient(socket); //添加新上线用户
  18. BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); //读取用户发送来的消息
  19. String msg = null;
  20. while ( (msg = reader.readLine())!=null ){
  21. String forMsg = "客户端["+socket.getPort()+"]"+msg+"\n";
  22. System.out.println(forMsg);
  23. /*
  24. *将收到的消息转发给聊天室里在线的其他用户
  25. *由于该方法读取的消息也是由readLine读取,而该方法要读取到\n才可以接受读取
  26. *因此需要在msg后面加上\n,用于使读取接受
  27. */
  28. server.forwardMessage(socket,forMsg);
  29. /* 检查用户是否准备退出 */
  30. if (server.readyToQuit(msg)){
  31. break;
  32. }
  33. }
  34. }catch (IOException e) {
  35. e.printStackTrace();
  36. }finally {
  37. try {
  38. //如果该用户退出聊天,也要将该用户的key、value从map中删除
  39. server.removeClient(socket);
  40. } catch (IOException e) {
  41. e.printStackTrace();
  42. }
  43. }
  44. }
  45. }

客户端:

  1. package chatroom;
  2. import java.io.*;
  3. import java.net.Socket;
  4. /* 用户端 */
  5. public class ChatClient {
  6. private final String DEFAULT_SERVER_HOST = "127.0.0.1";
  7. private final int DEFAULT_SERVER_PORT = 8888;
  8. private final String QUIT = "quit";
  9. private Socket socket;
  10. private BufferedReader reader;
  11. private BufferedWriter writer;
  12. /* 发送消息给服务器 */
  13. public void send(String msg) throws IOException {
  14. //判断该socket的输出流是否为开放的状态
  15. if (!socket.isOutputShutdown()){
  16. writer.write(msg+"\n");
  17. writer.flush();
  18. }
  19. }
  20. /* 从服务器接收消息 */
  21. public String receive() throws IOException {
  22. String msg = null;
  23. if (!socket.isInputShutdown()){
  24. msg = reader.readLine();
  25. }
  26. return msg;
  27. }
  28. /* 检查用户是否准备退出 */
  29. public boolean readyToQuit(String msg){
  30. return QUIT.equals(msg);
  31. }
  32. public void start(){
  33. try {
  34. //创建socket
  35. socket = new Socket(DEFAULT_SERVER_HOST,DEFAULT_SERVER_PORT);
  36. //创建用来发送和接收信息的io流
  37. reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
  38. writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
  39. //处理用户的输入
  40. new Thread(new UserInputHander(this)).start();
  41. //读取服务器转发的各种信息
  42. String msg = null;
  43. while ((msg=receive())!=null){
  44. System.out.println(msg);
  45. }
  46. } catch (IOException e) {
  47. e.printStackTrace();
  48. } finally {
  49. if (writer!=null){
  50. try {
  51. System.out.println("关闭socket");
  52. writer.close();
  53. } catch (IOException e) {
  54. e.printStackTrace();
  55. }
  56. }
  57. }
  58. }
  59. public static void main(String[] args) {
  60. ChatClient chatClient = new ChatClient();
  61. chatClient.start();
  62. }
  63. }

客户端用户输入方法:

  1. package chatroom;
  2. import java.io.BufferedReader;
  3. import java.io.IOException;
  4. import java.io.InputStreamReader;
  5. /* 处理用户在控制台上的输入 */
  6. public class UserInputHander implements Runnable{
  7. private ChatClient chatClient;
  8. public UserInputHander(ChatClient chatClient) {
  9. this.chatClient = chatClient;
  10. }
  11. @Override
  12. public void run() {
  13. try {
  14. BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in)); //等待用户输入信息
  15. while (true){
  16. String input = consoleReader.readLine();
  17. chatClient.send(input); //向服务器发送消息
  18. /* 检查用户是否准备退出 */
  19. if (chatClient.readyToQuit(input)){
  20. break;
  21. }
  22. }
  23. } catch (IOException e) {
  24. e.printStackTrace();
  25. }
  26. }
  27. }

测试:

启动 ChatServer 后启动多个 ChatClient 实例,在不同的 ChatClient 实例窗口中模拟消息发送,每个窗口都可以收到其他用户的消息
image.pngimage.pngimage.png
聊天室测试图示