1.写服务器端
因为是简单的网络聊天,所以我们需要服务器端来处理发送的信息
这个是我们的服务器处理图,我们在这里省略了注册步骤,因为在后期的Java应用中,我们基本用不到Swing所以这里我们直接使用下面的控制台进行输入输出。
需求分析
因为我们需要发送信息,所以我们至少需要两个集合,一个用来接收信息并将其列入队列,以便在处理的时候我们不出现输出序列问题,我们是聊天程序,所以不会是一个人聊天,我们需要一个用户集合,来保存用户的数据,方便我们知道是谁发送的消息,以及用户的IP,端口等等。
这里就是生产关系和消费关系,我们需要一个生产消息的队列和一个消费消息的队列。
代码如下:
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.创建接收线程
因为接收线程只在服务器端使用,所以单独写出来没有意义,秉承着数据简单,我们使用数据专家模式,将方法写在离数据最近的地方,我们就将它西在服务器里面,写成一个内部类。
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{
// this.messageQueue = messageQueue;
// }
public void run(){
}
}
}
下面我们先把接收线程的具体工作放一下,考虑一下接收线程中的Socket怎么得到的呢?
显然需要我们之前学过的监听方法。
4.添加监听客户端连接的代码
Socket.accept()就是我们之前学习过的监听客户端连接的方法,它的返回值是一个Socket类型的,而监听它的是ServerSocket类型,ServerSocket会申请一个端口和地址,当有服务器连接到这个地址和端口的时候,使用accept就能将它和服务器连接在一起。
/**
* 监听
*/
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.完成接收服务线程
在网络上进行传输就是对流的使用,因为我们只是做聊天系统,所以我们只需要选择字符流,并且Buffer字符流的readLine()非常好用,所以选择它。
!!注意:socket只能得到字节流,所以要把它包装成字符流得用InputStreamReadedr再包装一下
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();
}
}
}
我们可以在start刚开始的时候就启动发送消息的线程。防止在后面输入时消息发送不正确。
7.思考线程协做的问题
在一般的网络数据传输中,我们都将数据转化为JSON的形式发送以及接收。我们在这里举一个例子,来说明JSON对象的传输。
import net.sf.json.JSONObject;
import org.junit.Test;
public class Testjson {
@Test
public void Test1(){
Studnet s=new Studnet("zhangsan",12,1001);
JSONObject js= JSONObject.fromObject(s);
System.out.println(js.toString());
}
@Test
public void Test2(){
String jsstr="{\"age\":12,\"namber\":1001,\"name\":\"zhangsan\"}";
JSONObject object=JSONObject.fromObject(jsstr);
Studnet o= (Studnet) JSONObject.toBean(object,Studnet.class);
System.out.println(o);
}
}
其中Student类为:
public class Studnet {
private String name;
private int age;
private int namber;
@Override
public String toString() {
return "Studnet{" +
"name='" + name + '\'' +
", age=" + age +
", namber=" + namber +
'}';
}
public Studnet(String name, int age, int namber) {
this.name = name;
this.age = age;
this.namber = namber;
}
public Studnet(){}
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;
}
public int getNamber() {
return namber;
}
public void setNamber(int namber) {
this.namber = namber;
}
}
我们将输出结果发出来:
这里我们就将Student对象转换为JSON对象,并且将它转换回来了。
因为我们要频繁的使用将字符串和JSON对象相互转换的方法,所以我们将这个方法单独写一个工具包,将转换的方法放进去。
package edu.xatu.util;
import net.sf.json.JSONObject;
public class JSONUtil {
/**
* 对象转json的方法
* @return
*/
public static String boj2json(Object obj){
JSONObject ob=JSONObject.fromObject(obj);
return ob.toString();
}
public static <T> T json2obj(String jsonstr,Class<T> t){
JSONObject object=JSONObject.fromObject(jsonstr);
return (T)JSONObject.toBean(object,t);
}
}
以及消息的标识符:
package edu.xatu.util;
import java.util.Date;
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;
}
}
编写服务端启动类
public class SeverStart {
public static void main(String[] args) throws Exception {
new ChatSever().start();
}
}
9.写客户端
9.1客户端知道服务器的地址和端口,先编写客户端类直接连接服务器
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.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()));
String jsonStr=br.readLine();
//把json串转换成对象
MessageVO mvo= JSONUtil.json2obj(jsonStr,MessageVO.class);
//在控制台输出
System.out.println("info:"+mvo.getMesg()+"【时间:"+mvo.getDate()+"】");
}
//再监听有没有下一个
} catch (IOException e) {
e.printStackTrace();
}
}
}
2.创建键盘监听类,并发向服务器
private class SendService extends Thread{
private PrintWriter pw=null;
public void run(){
try {
while (true) {
Scanner in=new Scanner(System.in);
//监听键盘消息
String mesg=in.nextLine();
//封装MessageVo
MessageVO vo=new MessageVO(mesg,new Date());
//解析成json串
String jsonstr=JSONUtil.boj2json(vo);
//发送到服务器
pw=new PrintWriter(s.getOutputStream());
pw.println(jsonstr);
pw.flush();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
最后,要在客户端启动两个线程,这就是两个用户。
public class ChatClient {//客户端
/*
*聊天服务器的地址和端口
*/
Socket s=null;
private String addr="127.0.0.1";
private static int port=8888;
public void start(){
try {
s=new Socket(addr,port);
//知道地址和端口,直接创建套接字
//启动两个监听服务线程
new ReceiveService().start();
new SendService().start();
} catch (IOException e) {
e.printStackTrace();
}
}
两个监听线程均依赖网络套接字,所以启动线程的代码写在创建套接后就可以
最后 ,编写客户端的启动类
public class ClientStart {
public static void main(String[] args) {
new ChatClient().start();
}
}
我们现在将程序运行:
客户端1:
客户端2:
服务器端: