Java-全双工聊天服务器(以及客户端)
完整代码在最下边
设计思路
说到聊天工具,让人不得不想到QQ。那么我们在QQ上聊天的时候究竟经历了哪些过程呢?
登录
每个人都有自己的账号密码,据此才能登录上QQ的服务器。所以我们的聊天服务器也必须有这项过程。那么据此,我们就需要一个容器在服务端去存储用户账号密码,此外我们还需要实现一个用户验证的模块。
找到聊天对象
服务端应该有一个功能:即告诉各个客户端当前有哪些人在线,这样才能保住信息能够被送达。所以我们需要实现一些命令,使得服务端除了转发信息外还可以发送一些额外信息给客户端。
发送信息
当我们点击发送之后,信息实际上是被发送到了服务端中,然后再根据信息中的附加信息进去转发。所以我们需要解决如何给信息附加额外信息的问题,以及如何解析这些信息。
此外,还有一个问题需要考虑:对于需要转发的信息,服务器是立即进行转发还是先存下来轮询转发。在这里我使用的是消息队列,即服务端先将接收到的消息存到队列中,每隔一段时间轮询此队列进行发送。
总结
为了实现全双工的聊天服务器,我们需要:
- 信息处理模块
- 客户验证模块(服务端)
- 在线用户模块(服务端)
- 信息收发模块
信息处理模块
信息绝大部分是由客户端发送到服务端进行处理的,故而发送的信息应具有额外的说明信息。对于此次的全双工聊天服务器开发,将信息数据格式定为:
(接收者)@-(信息主体)@-(发送者)
例如,Jack通过客户端要向Nancy发送信息“I am Jack”,那么服务端接受到的字符串为“Nancy@-I am Jack@-Jack”.随后,服务端将对此字符串进行验证分析:接收者为Nancy,内容为I am Jack,发送者为Jack。判断字符串拥有完整含义并分析完毕后将此消息投入消息队列中,等待发送。
消息队列
为了提高服务端执行效率,加入了消息队列,此为一个单独的线程。作用为将待转发的信息发送。
工作流程如下:每隔一段时间查询此队列,若不为空,则轮询队列。使用读取到的信息进行查找,若找到对应用户名的Socket,则转入发送信息收发模块的进行发送程序。若未找到,则无应答。
客户端对服务端可以发送一些特殊信息,即消息接收者为服务端。服务端保留特殊字符串作为服 务端对客户端的消息应答。如下所示:
1.Time:获取服务器时间
2.Users:获取当前在线用户名单
3.(待添加…)
//消息队列
public static List<Message> waitForSend;
//信息解析
public void analyseToMessage(String data) {
String toWho=data.split("@-")[0];
String content=data.split("@-")[1];
String who=data.split("@-")[2];
Message message=new Message(toWho, content,who);
if ("Server".equals(toWho)) {
beats.add(message);
return;
}
waitForSend.add(message);
}
public void analyseToMessage(Message m) {
waitForSend.add(m);
}
客户验证模块
在每次用户使用客户端连接服务端的时候触发此模块。
作用为:对当前连接的客户端进行验证。由于并不存在密码系统,故而只验证用户输入的用户名是否与当前在线用户名单上的用户重名。若重名,则返回错误信息并要求重新输入用户名。
此处可进行拓展:例如加入密码系统。这样,在用户第一次登陆时将进行注册程序,将客户端输入的用户名及密码存入数据库中。下次用户登录时,直接输入用户名和密码即可。也可不使用数据库,保存到txt文件也可。不过为了安全和效率起见,还是使用数据库比较好。
此外,尤其对于客户验证模块以及信息收发模块来说,具有修改此模块的权限。客户验证成功之后,将调用相关方法将用户加入到此模块中。信息收发模块下的心跳模块将对心跳检测不通过的客户端进行删除,通过调用此模块的方式。
static class Authorized implements Runnable{
Socket s;
public Authorized(Socket s){
this.s=s;
}
@Override
public void run() {
try {
while (true) {
Writer writer = new OutputStreamWriter(s.getOutputStream());
BufferedReader reader = new BufferedReader(new InputStreamReader(s.getInputStream()));
writer.write("Please Input Your Name:\n");
writer.flush();
String name;
while (true) {
if ((name = reader.readLine()) != null) {
break;
}
}
if(serverHandler.getOnlineUsers().isRepeat(name)) {
writer.write("User exists,please rename\n");
writer.flush();
continue;
} else {
serverHandler.getOnlineUsers().addUser(s,name);
Message m=new Message(name,"Hello","Server");
serverHandler.analyseToMessage(m);
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
static class CheckBeats implements Runnable{
int time=0;
public CheckBeats(int delayTime){
time=delayTime;
}
@Override
public void run() {
while (true){
try {
Thread.sleep(time);
for (int i=0;i<serverHandler.getOnlineUsers().getOnlineUsers().size();i++){
for(Message m:serverHandler.getBeats()){
if(serverHandler.getOnlineUsers().getOnlineUserList().contains(m.getWho())){
continue;
}
else {
serverHandler.getOnlineUsers().userLogout(serverHandler.getOnlineUsers().getOnlineUsers().get(i).getName());
}
}
}
serverHandler.initialBeats();
} catch (InterruptedException | IOException e) {
e.printStackTrace();
}
}
}
}
信息收发模块
此模块在客户端以及服务端都存在且大同小异。
其主要表现为Send和Recive两个线程,作用为提供收发信息的服务。对于服务端来说,收到的信息将交付信息处理模块进行处理,发送的信息将由目标客户端进行接收。对于客户端来说:收到的消息将呈现在用户眼中,发送的信息将由服务端接收。
static class Send implements Runnable{
@Override
public void run() {
while(true) {
if(ServerHandler.waitForSend!=null&&ServerHandler.waitForSend.size()!=0) {
try {
serverHandler.send();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
static class Recive implements Runnable{
@Override
public void run() {
while(true) {
if(serverHandler.getOnlineUsers().getOnlineUserList().size()!=1) {
for(int i=1;i<serverHandler.getOnlineUsers().getOnlineUsers().size();i++) {
try {
BufferedReader reader=new BufferedReader(new InputStreamReader(serverHandler.getOnlineUsers().getOnlineUsers().get(i).getSocket().getInputStream()));
String temp;
String data="";
if(reader.ready()&&(data=reader.readLine())!=null) {
//Simple judge whether the format of data is correct
if(!Message.analyseFormat(data)){
continue;
}
//Simple judge the user who sent is themselves
if(!data.split("@-")[2].equals(serverHandler.getOnlineUsers().getOnlineUsers().get(i).getName())){
continue;
}
serverHandler.analyseToMessage(data.replaceAll("\n",""));
}
else {
continue;
}
} catch (IOException e) {
System.out.println("Nothing");
}
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
完整代码
ServerHandler.java
package zhl.ServerHandler;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import zhl.OnlineUser.*;
public class ServerHandler {
public static OnlineUser onlineUser;
public static List<Message> waitForSend;
public static List<Message> beats;
public ServerHandler() {
onlineUser=new OnlineUser();
waitForSend=new ArrayList<Message>();
onlineUser.addUser(new Socket(), "Server");
beats=new ArrayList<Message>();
beats.add(new Message("Server","alive","Server"));
}
/**
* send message to users
* @throws IOException
*/
public void send() throws IOException {
int p=0;
Writer writer;
for (int i=0;i<waitForSend.size();i++){
Message m=waitForSend.get(p);
//When client send message to Server
if(m.getToWho().equals("Server")){
System.out.println("["+m.getWho()+"]"+m.getContent());
waitForSend.remove(p);
continue;
}
Socket client= onlineUser.getUser(m.getToWho());
//If do not exist User
if(client==null){
waitForSend.add(new Message(m.getWho(),"User not exists\n","Server"));
waitForSend.remove(p);
continue;
}
writer=new OutputStreamWriter(client.getOutputStream());
writer.write(m.getContent());
System.out.println("Send: "+m.getToWho()+" "+m.getContent());
writer.flush();
waitForSend.remove(p);
}
}
public void analyseToMessage(String data) {
String toWho=data.split("@-")[0];
String content=data.split("@-")[1];
String who=data.split("@-")[2];
Message message=new Message(toWho, content,who);
if ("Server".equals(toWho)) {
beats.add(message);
return;
}
waitForSend.add(message);
}
public void analyseToMessage(Message m) {
waitForSend.add(m);
}
public OnlineUser getOnlineUsers() {
return onlineUser;
}
public List<Message> getBeats(){
return beats;
}
public void initialBeats(){
beats=new ArrayList<Message>();
beats.add(new Message("Server","alive","Server"));
}
}
Message.java
package zhl.OnlineUser;
public class Message {
private String toWho;
private String content;
private String who;
public Message(String toWho,String content,String who) {
this.toWho=toWho;
this.content=content+"\n";
this.who=who;
}
public static boolean analyseFormat(String data){
int sl=3;
return data.split("@-").length == sl;
}
public String getToWho() {
return toWho;
}
public String getContent() {
return content;
}
public void setToWho(String toWho) {
this.toWho=toWho;
}
public void setContent(String content) {
this.content=content;
}
public void setWho(String who) {
this.who=who;
}
public String getWho() {
return who;
}
}
OnlineUser.java
package zhl.OnlineUser;
import java.io.IOException;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
public class OnlineUser {
private List<User> onlineUser;
int ordinal=0;
public OnlineUser() {
onlineUser=new ArrayList<User>();
}
public void addUser(Socket s,String name) {
User u=new User(s, name);
onlineUser.add(u);
System.out.println(name+" Online");
}
public void userLogout(String name) throws IOException {
for(int i=0;i<onlineUser.size();i++) {
if(onlineUser.get(i).getName().equals(name)) {
onlineUser.get(i).getSocket().close();
onlineUser.remove(i);
break;
}
}
}
public Socket getUser(String name) throws IOException {
for (User user : onlineUser) {
if (user.getName().equals(name)) {
return user.getSocket();
}
}
return null;
}
public List<String> getOnlineUserList(){
List<String> nameList=new ArrayList<String>();
for (User user : onlineUser) {
nameList.add(user.getName());
}
return nameList;
}
public List<User> getOnlineUsers(){
return onlineUser;
}
public boolean isRepeat(String name) throws IOException {
for(User u:onlineUser){
if(u.getName().equals(name)) {
return true;
}
}
return false;
}
}
User.java
package zhl.OnlineUser;
import java.net.Socket;
public class User {
private String name;
private Socket s;
public User(Socket s,String name) {
this.name=name;
this.s=s;
}
public String getName() {
return name;
}
public Socket getSocket() {
return s;
}
}
Server.java
package zhl.Server;
import java.awt.*;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import zhl.OnlineUser.Message;
import zhl.ServerHandler.ServerHandler;
/**
* @author zhl
*/
public class Server {
private static ServerHandler serverHandler;
private ServerSocket serverSocket;
private Server() {
//Only once
serverHandler=new ServerHandler();
}
private void startAccept() throws IOException {
try {
serverSocket=new ServerSocket(4755);
Send send= new Send();
Recive recive= new Recive();
CheckBeats cb=new CheckBeats(6000);
ExecutorService cached= Executors.newCachedThreadPool();
cached.execute(send);
cached.execute(recive);
cached.execute(cb);
//Start Listening
while(true) {
Socket socket= serverSocket.accept();
Authorized authorized= new Authorized(socket);
cached.execute(authorized);
}
}
finally {
serverSocket.close();
}
}
/**
* For Test
* @throws IOException
*
*/
public static void main(String[] args) throws IOException {
System.out.println("Launching");
Server server=new Server();
System.out.println("Listening");
server.startAccept();
}
static class Send implements Runnable{
@Override
public void run() {
while(true) {
if(ServerHandler.waitForSend!=null&&ServerHandler.waitForSend.size()!=0) {
try {
serverHandler.send();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
static class Recive implements Runnable{
@Override
public void run() {
while(true) {
if(serverHandler.getOnlineUsers().getOnlineUserList().size()!=1) {
for(int i=1;i<serverHandler.getOnlineUsers().getOnlineUsers().size();i++) {
try {
BufferedReader reader=new BufferedReader(new InputStreamReader(serverHandler.getOnlineUsers().getOnlineUsers().get(i).getSocket().getInputStream()));
String temp;
String data="";
if(reader.ready()&&(data=reader.readLine())!=null) {
//Simple judge whether the format of data is correct
if(!Message.analyseFormat(data)){
continue;
}
//Simple judge the user who sent is themselves
if(!data.split("@-")[2].equals(serverHandler.getOnlineUsers().getOnlineUsers().get(i).getName())){
continue;
}
serverHandler.analyseToMessage(data.replaceAll("\n",""));
}
else {
continue;
}
} catch (IOException e) {
System.out.println("Nothing");
}
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
static class Authorized implements Runnable{
Socket s;
public Authorized(Socket s){
this.s=s;
}
@Override
public void run() {
try {
while (true) {
Writer writer = new OutputStreamWriter(s.getOutputStream());
BufferedReader reader = new BufferedReader(new InputStreamReader(s.getInputStream()));
writer.write("Please Input Your Name:\n");
writer.flush();
String name;
while (true) {
if ((name = reader.readLine()) != null) {
break;
}
}
if(serverHandler.getOnlineUsers().isRepeat(name)) {
writer.write("User exists,please rename\n");
writer.flush();
continue;
} else {
serverHandler.getOnlineUsers().addUser(s,name);
Message m=new Message(name,"Hello","Server");
serverHandler.analyseToMessage(m);
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
static class CheckBeats implements Runnable{
int time=0;
public CheckBeats(int delayTime){
time=delayTime;
}
@Override
public void run() {
while (true){
try {
Thread.sleep(time);
for (int i=0;i<serverHandler.getOnlineUsers().getOnlineUsers().size();i++){
for(Message m:serverHandler.getBeats()){
if(serverHandler.getOnlineUsers().getOnlineUserList().contains(m.getWho())){
continue;
}
else {
serverHandler.getOnlineUsers().userLogout(serverHandler.getOnlineUsers().getOnlineUsers().get(i).getName());
}
}
}
serverHandler.initialBeats();
} catch (InterruptedException | IOException e) {
e.printStackTrace();
}
}
}
}
}
Client.java
package zhl.Server;
import java.io.*;
import java.net.*;
import java.util.Scanner;
public class Client {
static Socket client;
public static void main(String[] args)
{
try
{
client=new Socket("127.0.0.1",4755);
//client=new Socket("47.92.165.73",4755);
Recive recive=new Recive();
Send send=new Send();
Thread sed =new Thread(send);
Thread rec =new Thread(recive);
rec.start();
sed .start();
}
catch (IOException e)
{
//System.out.println(e.getMessage());
}
}
static class Recive implements Runnable{
BufferedReader reader;
Recive() throws IOException {
reader= new BufferedReader(new InputStreamReader(client.getInputStream()));
}
@Override
public void run() {
String temp=null;
String data="";
while(true) {
try {
if ((temp=reader.readLine())!=null)
{
data+=temp;
}
} catch (IOException e) {
e.printStackTrace();
}
if(data!="")
{
System.out.println(data);
}
data="";
}
}
}
static class Send implements Runnable{
@Override
public void run() {
while (true){
try {
Writer writer=new OutputStreamWriter(client.getOutputStream());
BufferedReader reader=new BufferedReader(new InputStreamReader(System.in));
writer.write(reader.readLine()+"\n");
writer.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}