综合案例-聊天程序
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重复了。修改
正常