综合案例-聊天程序

综合案例-聊天程序

在这里插入图片描述

1.写服务端

在这里插入图片描述

1.1在类中添加消息队列及Socket集合

因为需要给所有客户端发送消息,所以服务器端必须持有所有客户端Socket的集合

生产和消费消息数据需要一个消息队列,所以服务器还必须定义一个消息队列

package edu.xalead.server;

import java.net.Socket;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;

public class ChatServer {
    /**
     * 客户端连接集合
     */
    private ConcurrentHashMap<String, Socket> allCustomer = new ConcurrentHashMap<>();

    /**
     * 存放消息的队列
     */
    private ConcurrentLinkedQueue<String> messageQueue = new ConcurrentLinkedQueue<>();
}
2.创建接收线程

离开ChatServer类没有利用价值,所以我这里写成内部类

package edu.xalead.server;

import java.net.Socket;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;

public class ChatServer {
    /**
     * 所以客户端连接集合
     */
    private ConcurrentHashMap<String, Socket> allCustomer = new ConcurrentHashMap<>();

    /**
     * 存放消息的队列
     */
    private ConcurrentLinkedQueue<String> messageQueue = new ConcurrentLinkedQueue<>();

    /**
     * 创建接收线程
     * 内部类因为已经可以访问外部类的所有属性和方法,所以没必要再创建
     */
    private class ReceiveService extends Thread{
//        /**
//         * 持有消息队列的引用
//         * 内部类因为已经可以访问外部类的所有属性和方法,所以没必要再创建引用
//         */
//        private ConcurrentLinkedQueue<String> messageQueue = null;
//        private ReceiveService(ConcurrentLinkedQueue<String> messageQueue){
//            this.messageQueue = messageQueue;
//        }
        
        public void run(){

        }
    }
}

3.接收客户消息

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

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

下面我们先把接收线程的具体工作放一下,思考接收线程中的Socket怎么得到呢?

显然,需要编写监听客户端的代码

4.添加监听客户端连接的代码
	/**
     * 监听
     */
    public void start(){
        ServerSocket serverSocket = null;
        Socket client = null;
        try {
            //申请端口
            serverSocket = new ServerSocket(port);
            while (true) {
                //监听客户端连接
                System.out.println("开始监听新的客户端连接...");
                client = serverSocket.accept();
                System.out.println("监听到客户端【" + client.getInetAddress()
                       .getHostAddress() + ":" + client.getPort() + "】");
                //提供消息服务
                new ReceiveService(client).start();
                //把socket放进客户socket集合,以便发送线程使用
                allCustomer.put(client.getInetAddress().getHostAddress(),client);
                //监听下一个
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
5.完成接收服务线程
	public void run(){
            //因为接收字符所以选择字符流,并且Buffer字符流的readLine()非常好用,所以选择它
            BufferedReader br = null;
            try {
                //注意socket只能得到字节流,所以要把它包装成字符流得用InputStreamReadedr再包装一下
                br = new BufferedReader(
                        new InputStreamReader(client.getInputStream()));
                while (true) {
                    //接收消息
                    System.out.println("等待接收客户端【" + client.getInetAddress()
                                       .getHostAddress() + "】消息");
                    String mesg = br.readLine();
                    System.out.println("接收到客户端【" + client.getInetAddress()
                                       .getHostAddress() + "】消息【" + mesg + "】");
                    //放入消息队列
                    messageQueue.offer(mesg);
                    //接收下一条
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
6.定义发送线程
/**
     * 创建发送线程
     */
    private class SendService implements Runnable{

        @Override
        public void run() {
            try {
                PrintWriter pw = null;
                while (true) {
                    //取消息队列中的消息
                    String mesg = messageQueue.poll();
                    if(mesg != null) {
                        //遍历客户连接
                        for (Socket socket : allCustomer.values()) {
                            //创建字符输出流半配网络字节流
                            pw = new PrintWriter(socket.getOutputStream());
                            //向客户端发送消息
                            pw.println(mesg);
                            pw.flush();
                        }
                        //到队列里取下一条消息
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }


启动发送消息的线程
在这里插入图片描述

发送消息的线程在服务器启动时开启就可以

7.思考线程协作的问题

如果不考虑线程协作,那么发送消息线程在消息队列为空的时候仍然会做无意义循环,浪费宝贵的CPU时间片
在这里插入图片描述

所以我们要用线程协作解决这个问题。首先要添加同步块,因为消息队列是所有线程监控的同一对象,所以用它作为同步监视器
在这里插入图片描述

切记要注意同步块的范围,如果同步锁定紫色框选范围,则只要有一个线程br.readLine()会等待客户消息,导致所有接收消息的线程无法进入同步块,无法执行接收消息的工作
在这里插入图片描述
最后添加协作代码

当消息队列为空时,发送线程进入休眠状态
在这里插入图片描述
当接收消息线程接收到消息并放入消息队列,则唤醒发送线程
在这里插入图片描述

8.我们准备把传输数据改为json传输
8.1创建VO对象
public class MessageVO {
    private String mesg;
    private Date date;

    public MessageVO() {
    }

    public MessageVO(String mesg, Date date) {
        this.mesg = mesg;
        this.date = date;
    }

    public String getMesg() {
        return mesg;
    }

    public void setMesg(String mesg) {
        this.mesg = mesg;
    }

    public Date getDate() {
        return date;
    }

    public void setDate(Date date) {
        this.date = date;
    }
}
vo (view object)对象主要是保存界面间传输数据的对象
8.2创建JSON和对象互转工具类
package edu.xalead.util;

import net.sf.json.JSONObject;

public class JSONUtil {
    /**
     * 对象转json的方法
     * @return
     */
    public static String obj2json(Object obj){
        JSONObject ob = JSONObject.fromObject(obj);
        return ob.toString();
    }

    /**
     * 把json串转成对象的方法
     * @return
     */
    public static <T> T json2obj(String jsonStr,Class<T> t){
        JSONObject object = JSONObject.fromObject(jsonStr);
        return (T)JSONObject.toBean(object,t);
    }
}

编写测试代码如下
@Test
    public void test1(){
        //创建学生对象
        Student s = new Student();
        s.setNo(2222);
        s.setName("张三");
        s.setAge(20);

        Address adr = new Address();
        adr.setCollegeAddr("未央区");
        adr.setHomeAddr("大雁塔");
        s.setAddress(adr);
        System.out.println(JSONUtil.obj2json(s));

        String jsonStr = "{\"no\":2222,\"address\":{\"collegeAddr\":\"未央区\",\"homeAddr\":\"大雁塔\"},\"name\":\"张三\",\"age\":20}";
        Student ss = JSONUtil.json2obj(jsonStr,Student.class);
        System.out.println(ss);
    }

输出:

{"no":2222,"address":{"collegeAddr":"未央区","homeAddr":"大雁塔"},"name":"张三","age":20}

Student{no=2222, name='张三', age=20, address=Address{homeAddr='大雁塔', collegeAddr='未央区'}}

编写服务端启动类

package edu.xalead.server;

public class ServerStart {
    public static void main(String[] args) {
        new ChatServer().start();
    }
}
9.写客户端
9.1客户端知道服务器的地址和端口,先编写客户端类直连服务器
package edu.xalead.client;

import java.io.IOException;
import java.net.Socket;

public class ChatClient {
    /**
     * 聊天服务器的地址
     */
    private String addr = "127.0.0.1";
    /**
     * 聊天服务的端口
     */
    private int port = 9999;
    public void start(){

        Socket s = null;
        try {
            //客户知道服务器的地址和端口,直接创建套接字
            s = new Socket(addr,port);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

客户端要做两件事:

  • 1.监听服务器返回的消息,并输出到控制台

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

    很显然,这里需要两个客户线程

9.1创建客户端接收线程
  • 1.监听服务器返回的消息,并输出到控制台,因为离开客户端没有复用价值,所以我们也是写成ChatClient类的内部类
/**
     * 创建监听服务器消息线程
     */
    private class ReceiveService extends Thread{
        private BufferedReader br = null;
        public void run(){
            try {
                while (true) {
                    br = new BufferedReader(
                            new InputStreamReader(s.getInputStream()));
                    //监听服务器发送过来的json字符串
                    String jsonStr = br.readLine();
                    //json串转换成对象
                    MessageVO mvo = JSONUtil.json2obj(jsonStr,MessageVO.class);
                    //在控制台输出
                    System.out.println("info:" + mvo.getMesg() + " 【时间:】" + mvo.getDate());
                    //再监听有没有下一个
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
  • 2.监听键盘消息,并发向服务器
/**
     * 创建监听键盘的线程
     */
    private class SendService extends Thread{
        private PrintWriter pw = null;
        public void run(){
            try {
                Scanner scanner = new Scanner(System.in);
                while (true) {
                    //接收键盘消息
                    String mesg = scanner.nextLine();
                    //封装MessageVO
                    MessageVO vo = new MessageVO(mesg, new Date());
                    //解析成json串
                    String jsonStr = JSONUtil.obj2json(vo);
                    //发送到服务器
                    pw = new PrintWriter(s.getOutputStream());
                    pw.println(jsonStr);
                    pw.flush();
                    //监听键盘下一条
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

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

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

    package edu.xalead.client;
    
    public class ClientStart {
        public static void main(String[] args) {
            new ChatClient().start();
        }
    }
    
    
10.调试

测试过程发现只有一个客户端能接收到消息,我们用的map集合,所以怀疑服务器端套接字集合中可能只有一个客户端的套接字。检查代码,client.getInetAddress().getHostAddress()方法只返回127.0.0.1,不包括端口,显然是HashMap的key重复了。修改
在这里插入图片描述

正常

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值