【Java】聊天程序综合案例

拼搏现实的明天。 2021-12-01 14:38 343阅读 0赞

在这里插入图片描述
创建服务端
在类中添加消息队列及Socket集合
因为需要给所有客户端发送消息,所以服务器端必须持有所有客户端Socket的集合
生产和消费消息数据需要一个消息队列,所以服务器还必须定义一个消息队列

  1. package edu.xalead.server;
  2. import java.io.BufferedReader;
  3. import java.io.IOException;
  4. import java.io.InputStreamReader;
  5. import java.io.PrintWriter;
  6. import java.net.ServerSocket;
  7. import java.net.Socket;
  8. import java.util.concurrent.ConcurrentHashMap;
  9. import java.util.concurrent.ConcurrentLinkedQueue;
  10. public class chatServer {
  11. /**
  12. * 所有客户端连接集合
  13. */
  14. private ConcurrentHashMap<String, Socket> allCustomer = new ConcurrentHashMap<>();
  15. /***
  16. * 存放消息的队列
  17. */
  18. private ConcurrentLinkedQueue<String> messageQueue = new ConcurrentLinkedQueue<>();
  19. }

创建接收线程
离开ChatServer类没有利用价值,所以我这里写成内部类

  1. /**
  2. * 创建接受线程
  3. * 离开ChatServer后失去价值,故使用内部类
  4. * 作用:只做接受消息,放进消息队列
  5. */
  6. private class ReceiveService extends Thread{
  7. /**
  8. * 必须持有消息队列的引用
  9. */
  10. //如果需要创建效果更强的外部类则对于消息队列需要创建接口
  11. // private ConcurrentLinkedQueue<String> messageQueue=null;
  12. // private ReceiveService(ConcurrentLinkedQueue<String> messageQueue){
  13. // this.messageQueue=messageQueue;
  14. // }
  15. //客户端的套接字
  16. private Socket client=null;
  17. public ReceiveService(Socket client) {
  18. this.client = client;
  19. }
  20. public void run(){
  21. BufferedReader br=null;
  22. try {
  23. //注意socket只能得到字节流,所以要把它包装成字符流得用InputStreamReader包装一下
  24. br=new BufferedReader(
  25. new InputStreamReader(client.getInputStream()));
  26. while (true) {
  27. //接收消息
  28. System.out.println("等待接收客户端【"+client.getInetAddress().getHostAddress()
  29. +"】消息");
  30. String mesg=br.readLine();
  31. System.out.println("接收到客户端【"+client.getInetAddress().getHostAddress()
  32. +"】消息");
  33. //放入消息队列
  34. synchronized (messageQueue) {
  35. messageQueue.offer(mesg);
  36. messageQueue.notify();
  37. }
  38. //接受下一条
  39. }
  40. } catch (IOException e) {
  41. e.printStackTrace();
  42. }
  43. }
  44. }

.接收客户消息
每个接收线程只能为一个特定客户服务,必须持有这个客户的Socket,所以在接收线程中添加客户的 Socket引用

  1. //客户端的套接字
  2. private Socket client=null;
  3. public ReceiveService(Socket client) {
  4. this.client = client;
  5. }

接收线程中的Socket怎么得到呢?
显然,需要编写监听客户端的代码吧 4.添加监听客户端连接的代码

  1. private static final int port=9999;
  2. /**
  3. * 监听
  4. */
  5. public void start(){
  6. //启动发送消息线程
  7. new Thread(new SendService()).start();
  8. ServerSocket serverSocket=null;
  9. Socket client=null;
  10. try {
  11. //申请端口
  12. serverSocket =new ServerSocket(port);
  13. while (true) {
  14. //监听
  15. System.out.println("开始监听新的客户端连接 。。。。");
  16. client=serverSocket.accept();
  17. System.out.println("监听到客户端【"+client.getInetAddress().getHostAddress()
  18. +":"+client.getPort()+"】");
  19. //提供消息服务
  20. new ReceiveService(client).start();
  21. //把socket放进客户socket集合,以便发送线程使用
  22. String key=client.getInetAddress().getHostAddress()+":"+client.getPort();
  23. System.out.println(key);
  24. allCustomer.put(key,client);
  25. //监听下一个
  26. }
  27. } catch (Exception e) {
  28. e.printStackTrace();
  29. }
  30. }

定义发送线程

  1. /**
  2. * 创建发送线程
  3. */
  4. private class SendService implements Runnable{
  5. @Override
  6. public void run() {
  7. try {
  8. PrintWriter pw=null;
  9. while (true) {
  10. //取消息队列中的消息
  11. String mesg=messageQueue.poll();//poll取一个删一个
  12. synchronized (messageQueue) {
  13. if(mesg!=null) {
  14. //遍历客户端连接
  15. for (Socket socket : allCustomer.values()) {
  16. //创建字符输出流半配网络字节流
  17. pw = new PrintWriter(socket.getOutputStream());
  18. //向客户端发送消息
  19. pw.println(mesg);
  20. pw.flush();
  21. }
  22. }else {
  23. //休息
  24. messageQueue.wait();
  25. }
  26. }
  27. }
  28. //到队列里取下一条消息
  29. } catch (Exception e) {
  30. e.printStackTrace();
  31. }
  32. }
  33. }

启动发送消息线程
在这里插入图片描述
完整服务端代码

  1. package edu.xalead.server;
  2. import java.io.BufferedReader;
  3. import java.io.IOException;
  4. import java.io.InputStreamReader;
  5. import java.io.PrintWriter;
  6. import java.net.ServerSocket;
  7. import java.net.Socket;
  8. import java.util.concurrent.ConcurrentHashMap;
  9. import java.util.concurrent.ConcurrentLinkedQueue;
  10. public class chatServer {
  11. private static final int port=9999;
  12. /**
  13. * 监听
  14. */
  15. public void start(){
  16. //启动发送消息线程
  17. new Thread(new SendService()).start();
  18. ServerSocket serverSocket=null;
  19. Socket client=null;
  20. try {
  21. //申请端口
  22. serverSocket =new ServerSocket(port);
  23. while (true) {
  24. //监听
  25. System.out.println("开始监听新的客户端连接 。。。。");
  26. client=serverSocket.accept();
  27. System.out.println("监听到客户端【"+client.getInetAddress().getHostAddress()
  28. +":"+client.getPort()+"】");
  29. //提供消息服务
  30. new ReceiveService(client).start();
  31. //把socket放进客户socket集合,以便发送线程使用
  32. String key=client.getInetAddress().getHostAddress()+":"+client.getPort();
  33. System.out.println(key);
  34. allCustomer.put(key,client);
  35. //监听下一个
  36. }
  37. } catch (Exception e) {
  38. e.printStackTrace();
  39. }
  40. }
  41. /**
  42. * 所有客户端连接集合
  43. */
  44. private ConcurrentHashMap<String, Socket> allCustomer = new ConcurrentHashMap<>();
  45. /***
  46. * 存放消息的队列
  47. */
  48. private ConcurrentLinkedQueue<String> messageQueue = new ConcurrentLinkedQueue<>();
  49. /**
  50. * 创建发送线程
  51. */
  52. private class SendService implements Runnable{
  53. @Override
  54. public void run() {
  55. try {
  56. PrintWriter pw=null;
  57. while (true) {
  58. //取消息队列中的消息
  59. String mesg=messageQueue.poll();//poll取一个删一个
  60. synchronized (messageQueue) {
  61. if(mesg!=null) {
  62. //遍历客户端连接
  63. for (Socket socket : allCustomer.values()) {
  64. //创建字符输出流半配网络字节流
  65. pw = new PrintWriter(socket.getOutputStream());
  66. //向客户端发送消息
  67. pw.println(mesg);
  68. pw.flush();
  69. }
  70. }else {
  71. //休息
  72. messageQueue.wait();
  73. }
  74. }
  75. }
  76. //到队列里取下一条消息
  77. } catch (Exception e) {
  78. e.printStackTrace();
  79. }
  80. }
  81. }
  82. /**
  83. * 创建接受线程
  84. * 离开ChatServer后失去价值,故使用内部类
  85. * 作用:只做接受消息,放进消息队列
  86. */
  87. private class ReceiveService extends Thread{
  88. /**
  89. * 必须持有消息队列的引用
  90. */
  91. //如果需要创建效果更强的外部类则对于消息队列需要创建接口
  92. // private ConcurrentLinkedQueue<String> messageQueue=null;
  93. // private ReceiveService(ConcurrentLinkedQueue<String> messageQueue){
  94. // this.messageQueue=messageQueue;
  95. // }
  96. //客户端的套接字
  97. private Socket client=null;
  98. public ReceiveService(Socket client) {
  99. this.client = client;
  100. }
  101. public void run(){
  102. BufferedReader br=null;
  103. try {
  104. //注意socket只能得到字节流,所以要把它包装成字符流得用InputStreamReader包装一下
  105. br=new BufferedReader(
  106. new InputStreamReader(client.getInputStream()));
  107. while (true) {
  108. //接收消息
  109. System.out.println("等待接收客户端【"+client.getInetAddress().getHostAddress()
  110. +"】消息");
  111. String mesg=br.readLine();
  112. System.out.println("接收到客户端【"+client.getInetAddress().getHostAddress()
  113. +"】消息");
  114. //放入消息队列
  115. synchronized (messageQueue) {
  116. messageQueue.offer(mesg);
  117. messageQueue.notify();
  118. }
  119. //接受下一条
  120. }
  121. } catch (IOException e) {
  122. e.printStackTrace();
  123. }
  124. }
  125. }
  126. }

思考线程协作的问题
如果不考虑线程协作,那么发送消息线程在消息队列为空的时候仍然会做无意义循环,浪费宝贵的CPU 时间片
在这里插入图片描述
所以我们要用线程协作解决这个问题。首先要添加同步块,因为消息队列是所有线程监控的同一对象, 所以用它作为同步监视器
在这里插入图片描述
切记要注意同步块的范围,如果同步锁定紫色框选范围,则只要有一个线程br.readLine()会等待客户消 息,导致所有接收消息的线程无法进入同步块,无法执行接收消息的工作
在这里插入图片描述
最后添加协作代码
当消息队列为空时,发送线程进入休眠状态
watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQzNDE2MjI2_size_16_color_FFFFFF_t_70 5
当接收消息线程接收到消息并放入消息队列,则唤醒发送线程
在这里插入图片描述
.我们准备把传输数据改为json传输
创建VO对象

  1. package edu.xalead.vo;
  2. import java.util.Date;
  3. public class MessageVO {
  4. //vo(view object)
  5. private String mesg;
  6. private Date date;
  7. public MessageVO(){
  8. }
  9. public MessageVO(String mesg, Date date) {
  10. this.mesg = mesg;
  11. this.date = date;
  12. }
  13. @Override
  14. public String toString() {
  15. return "MessageVO{" +
  16. "mesg='" + mesg + '\'' +
  17. ", date=" + date +
  18. '}';
  19. }
  20. public String getMesg() {
  21. return mesg;
  22. }
  23. public void setMesg(String mesg) {
  24. this.mesg = mesg;
  25. }
  26. public Date getDate() {
  27. return date;
  28. }
  29. public void setDate(Date date) {
  30. this.date = date;
  31. }
  32. }

创建JSON和对象互转工具类

  1. package edu.xalead.util;
  2. import net.sf.json.JSONObject;
  3. public class JSONUtil {
  4. /**
  5. * 对象转json的方法
  6. * @return
  7. */
  8. public static String obj2json(Object obj){
  9. JSONObject ob=JSONObject.fromObject(obj);
  10. return ob.toString();
  11. }
  12. /**
  13. * 把json串转成对象的方法
  14. */
  15. public static <T> T json2obj(String jsonStr,Class<T> t){
  16. JSONObject object=JSONObject.fromObject(jsonStr);
  17. return (T)JSONObject.toBean(object,t);
  18. }
  19. }
  20. ______
  21. 测试代码
  22. package test.edu.xalead;
  23. import edu.xalead.util.JSONUtil;
  24. import net.sf.json.JSONObject;
  25. public class TestJSONUtil {
  26. @org.junit.Test
  27. public void test1(){
  28. //创建学生对象
  29. Student s=new Student();
  30. s.setNo(222);
  31. s.setAge(20);
  32. s.setName("zhansgan");
  33. Address adr=new Address();
  34. adr.setHomeadr("未央区");
  35. adr.setSchooladr("碑林区");
  36. s.setAddress(adr);
  37. System.out.println(JSONUtil.obj2json(s));
  38. String jsonStr="{\"address\":{\"homeadr\":\"央区\",\"schooladr\":\"碑林区\"},\"age\":20,\"name\":\"zhansgan\",\"no\":22}";
  39. Student ss=JSONUtil.json2obj(jsonStr,Student.class);
  40. System.out.println(ss);
  41. }
  42. }

编译结果
在这里插入图片描述
编写服务端启动类

  1. package edu.xalead.server;
  2. public class ServerStart {
  3. public static void main(String[] args) {
  4. new chatServer().start();
  5. }
  6. }

写客户端
客户端知道服务器的地址和端口,先编写客户端类直连服务

  1. package edu.xalead.client;
  2. import edu.xalead.util.JSONUtil;
  3. import edu.xalead.vo.MessageVO;
  4. import javax.xml.crypto.Data;
  5. import java.io.BufferedReader;
  6. import java.io.IOException;
  7. import java.io.InputStreamReader;
  8. import java.io.PrintWriter;
  9. import java.net.Socket;
  10. import java.util.Date;
  11. import java.util.Scanner;
  12. public class ChatClient {
  13. /**
  14. * 服务器地址
  15. */
  16. private String addr="127.0.0.1";
  17. Socket s=null;
  18. /**
  19. * 聊天服务端口
  20. */
  21. private int port=9999;
  22. public void start(){
  23. try {
  24. //客户知道服务器的地址和端口,直接创建套接字
  25. s=new Socket(addr,port);
  26. //启动两个监听服务线程
  27. new ReceiveService().start();
  28. new SendService().start();
  29. } catch (Exception e) {
  30. e.printStackTrace();
  31. }
  32. }
  33. }

客户端要做两件事:
1.监听服务器返回的消息,并输出到控制台
2.监听键盘消息,并发向服务器 很显然,这里需要两个客户线程

创建客户端接收线程
监听服务器返回的消息,并输出到控制台,因为离开客户端没有复用价值,所以我们也是写成 ChatClient类的内部类

  1. /**
  2. * 创建监听键盘消息
  3. */
  4. private class SendService extends Thread{
  5. private PrintWriter pw=null;
  6. public void run(){
  7. try {
  8. while (true) {
  9. Scanner scanner =new Scanner(System.in);
  10. //接受键盘消息
  11. String mesg=scanner.nextLine();
  12. //封装MessageVO
  13. MessageVO vo= new MessageVO(mesg, new Date());
  14. //解析成json串
  15. String jsonStr=JSONUtil.obj2json(vo);
  16. //发送到服务器
  17. pw=new PrintWriter(s.getOutputStream());
  18. pw.println(jsonStr);
  19. pw.flush();
  20. }
  21. } catch (Exception e) {
  22. e.printStackTrace();
  23. }
  24. }
  25. }

监听键盘消息,并发向服务器

  1. /**
  2. * 创建监听服务器消息线程
  3. */
  4. private class ReceiveService extends Thread{
  5. private BufferedReader br=null;
  6. public void run(){
  7. try {
  8. while (true) {
  9. br=new BufferedReader(
  10. new InputStreamReader(s.getInputStream()));
  11. //监听服务器发送过来的json字符串
  12. String jsonStr =br.readLine();
  13. //json串转换成对象
  14. MessageVO mvo= JSONUtil.json2obj(jsonStr,MessageVO.class);
  15. //在控制台输出
  16. System.out.println("info:"+mvo.getMesg()+"【时间:"+mvo.getDate()+"】");
  17. }
  18. } catch (Exception e) {
  19. e.printStackTrace();
  20. }
  21. }
  22. }

最后,客户端要启动两个服务线程
在这里插入图片描述
两个监听线程均依赖网络套接字,所以启动线程的代码写在创建套接后就可以

最后,编写客户端的启动类

  1. package edu.xalead.client;
  2. public class ClientStart {
  3. public static void main(String[] args) {
  4. new ChatClient().start();
  5. }
  6. }

结果
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

发表评论

表情:
评论列表 (有 0 条评论,343人围观)

还没有评论,来说两句吧...

相关阅读