项目开发||java聊天程序案例
项目实践:
首先,做这个项目的目的主要是为了巩固java学习的整个基础,包括面向对象的思想,流的应用,线程开发,网络编程的相关基础知识。
为了完成这个项目,首先我们需要清楚他的整个框架,建立聊天程序,是实现端对端的通信,这就需要建立一个服务器,通过这个服务器把多个客户端建立连接,实现聊天。
具体框图,如下:
一,服务端
在类中添加消息队列及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<>();
}
创建接收线程
离开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(){
}
}
}
接收客户消息
每个接收线程只能为一个特定客户服务,必须持有这个客户的Socket,所以在接收线程中添加客户的Socket引用
//客户端的套接字
private Socket client = null;
public ReceiveService(Socket client){
this.client = client;
}
下面我们先把接收线程的具体工作放一下,思考接收线程中的Socket怎么得到呢?
显然,需要编写监听客户端的代码吧
添加监听客户端连接的代码
/**
* 监听
*/
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();
}
}
完成接收服务线程
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();
}
}
}
定义发送线程
/**
* 创建发送线程
*/
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();
}
}
}
启动发送消息的线程
发送消息的线程在服务器启动时开启就可以
思考线程协作的问题
如果不考虑线程协作,那么发送消息线程在消息队列为空的时候仍然会做无意义循环,浪费宝贵的CPU时间片
所以我们要用线程协作解决这个问题。首先要添加同步块,因为消息队列是所有线程监控的同一对象,所以用它作为同步监视器
切记要注意同步块的范围,如果同步锁定紫色框选范围,则只要有一个线程br.readLine()会等待客户消息,导致所有接收消息的线程无法进入同步块,无法执行接收消息的工作
最后添加协作代码,当消息队列为空时,发送线程进入休眠状态
当接收消息线程接收到消息并放入消息队列,则唤醒发送线程
准备把传输数据改为json传输
创建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;
}
}
创建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.setName ("胡歌");
s.setId (140137);
s.setAge (36);
Address adr = new Address ();
adr.setHomeAddr ("上海市徐汇区");
adr.setSchoolAddr ("上海戏剧学院");
s.setAddress (adr);
System.out.println (JSONUtil.obj2json(s));
String jsonStr = "{\"address\":{\"homeAddr\":\"上海市徐汇区\",\"schoolAddr\":\"上海戏剧学院\"},\"age\":36,\"id\":140137,\"name\":\"胡歌\"}";
Student ss = JSONUtil.json2obj(jsonStr,Student.class);//toBean()方法把中间对象转换成对象
System.out.println (ss);
}
}
编写服务端启动类
package edu.xalead.server;
public class ServerStart {
public static void main(String[] args) {
new ChatServer().start();
}
}
———完整服务器代码:
chatserver:
package edu.xalead.server;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
public class ChatServer {
/**
* 服务器端口常量
*/
private static final int port = 5371;
/**
* 监听
*/
public void start(){
//启动发送消息线程
new Thread(new SendService()).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集合,以便发送线程使用
String key = client.getInetAddress().getHostAddress() + ":" + client.getPort();
allCustomer.put(key,client);
//监听下一个
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 所以客户端连接集合
*/
private ConcurrentHashMap<String, Socket> allCustomer = new ConcurrentHashMap<>();
/**
* 存放消息的队列
*/
private ConcurrentLinkedQueue<String> messageQueue = new ConcurrentLinkedQueue<>();
/**
* 创建发送线程
*/
private class SendService implements Runnable{
@Override
public void run() {
try {
PrintWriter pw = null;
while (true) {
//取消息队列中的消息
String mesg = messageQueue.poll();
synchronized (messageQueue) {
if(mesg != null) {
//遍历客户连接
for (Socket socket : allCustomer.values()) {
//创建字符输出流半配网络字节流
pw = new PrintWriter(socket.getOutputStream());
//向客户端发送消息
pw.println(mesg);
pw.flush();
}
//到队列里取下一条消息
}else{
//休息
messageQueue.wait();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 创建接收线程
*/
private class ReceiveService extends Thread{
// /**
// * 持有消息队列的引用
// */
// private ConcurrentLinkedQueue<String> messageQueue = null;
// private ReceiveService(ConcurrentLinkedQueue<String> messageQueue){
// this.messageQueue = messageQueue;
// }
//客户端的套接字
private Socket client = null;
public ReceiveService(Socket client){
this.client = client;
}
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 + "】");
//放入消息队列
synchronized (messageQueue) {
messageQueue.offer(mesg);
messageQueue.notify();
}
//接收下一条
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
MessageVo:
package edu.xalead.vo;
import java.util.Date;
public class MessageVo {
private String mesg;
private Date data;
public MessageVo(){
}
public MessageVo(String mesg, Date date){
this.mesg=mesg;
this.data=date;
}
public String getMesg() {
return mesg;
}
public void setMesg(String mesg) {
this.mesg = mesg;
}
public Date getDate() {
return data;
}
public void setDate(Date data) {
this.data = data;
}
}
Stuent类:
package test.edu.xalead;
public class Student {
private int id;
private String name;
private int age;
private Address address;
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
", address=" + address +
'}';
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
Address类:
package test.edu.xalead;
public class Address {
private String homeAddr;
private String schoolAddr;
@Override
public String toString() {
return "Address{" +
"homeAddr='" + homeAddr + '\'' +
", schoolAddr='" + schoolAddr + '\'' +
'}';
}
public String getHomeAddr() {
return homeAddr;
}
public void setHomeAddr(String homeAddr) {
this.homeAddr = homeAddr;
}
public String getSchoolAddr() {
return schoolAddr;
}
public void setSchoolAddr(String schoolAddr) {
this.schoolAddr = schoolAddr;
}
}
Test类:
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.setName ("胡歌");
s.setId (140137);
s.setAge (36);
Address adr = new Address ();
adr.setHomeAddr ("上海市徐汇区");
adr.setSchoolAddr ("上海戏剧学院");
s.setAddress (adr);
System.out.println (JSONUtil.obj2json(s));
String jsonStr = "{\"address\":{\"homeAddr\":\"上海市徐汇区\",\"schoolAddr\":\"上海戏剧学院\"},\"age\":36,\"id\":140137,\"name\":\"胡歌\"}";
Student ss = JSONUtil.json2obj(jsonStr,Student.class);//toBean()方法把中间对象转换成对象
System.out.println (ss);
}
}
启动类:
package edu.xalead.server;
public class ServerStart {
public static void main(String[] args) {
new ChatServer().start();
}
}
二,客户端
客户端知道服务器的地址和端口,先编写客户端类直连服务器
package edu.xalead.client;
import edu.xalead.util.JSONUtil;
import edu.xalead.vo.MessageVo;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Date;
import java.util.Scanner;
public class ChatClient {
/**
* 聊天服务器的地址
*/
private String addr = "127.0.0.1";
/**
* 连接服务器的套接字
*/
Socket s = null;
/**
* 聊天服务的端口
*/
private int port = 5371;
/**
* 启动客户端
*/
public void start(){
try {
//客户知道服务器的地址和端口,直接创建套接字
s = new Socket(addr,port);
//启动两个监听服务线程
new ReceiveService().start();
new SendService().start();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 创建监听键盘的线程
*/
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();
}
}
}
/**
* 创建监听服务器消息线程
*/
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();
}
}
}
}
客户端要做两件事:
- 1.监听服务器返回的消息,并输出到控制台
2.监听键盘消息,并发向服务器
很显然,这里需要两个客户线程
创建客户端接收线程
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();
}
}
}
监听键盘消息,并发向服务器
/**
* 创建监听键盘的线程
*/
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();
}
}
-———完整客户端代码:
ChatClient类:
package edu.xalead.client;
import edu.xalead.util.JSONUtil;
import edu.xalead.vo.MessageVo;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Date;
import java.util.Scanner;
public class ChatClient {
/**
* 聊天服务器的地址
*/
private String addr = "127.0.0.1";
/**
* 连接服务器的套接字
*/
Socket s = null;
/**
* 聊天服务的端口
*/
private int port = 5371;
/**
* 启动客户端
*/
public void start(){
try {
//客户知道服务器的地址和端口,直接创建套接字
s = new Socket(addr,port);
//启动两个监听服务线程
new ReceiveService().start();
new SendService().start();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 创建监听键盘的线程
*/
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();
}
}
}
/**
* 创建监听服务器消息线程
*/
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();
}
}
}
}
启动类:
package edu.xalead.client;
public class ClientStart {
public static void main(String[] args) {
new ChatClient().start();
}
}
三,调试
打开服务器
打开两个客户端后,服务器
两个客户端各自发送一条消息:
此时,服务器:
完成聊天通信!
还没有评论,来说两句吧...