思路
网络上的聊天本质上属于数据的交互,一个服务方负责接收客户端发来的信息,并进行进一步处理。因为此次实验是在本地实验,ip地址相同,所以封给stocket加了一个名字用于区别。
服务器端
服务器端的设计,采用多线程的方式。
主线程:负责监听客户端的连接,当监听到一个匹配的客户端时,调用注册线程将该客户端加入map集合中。
主线程代码
public class Server {
//存储客户端
static volatile Map<String,nameSocket> socketMap = new HashMap<String,nameSocket>();
//存储客户端姓名,将用该集合进行轮询操作
static volatile List<String> names = new ArrayList<>();
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(10086);
//启动轮询线程
new Thread(new lunxun()).start();
//等待客户端连接
while(true){
//包装客户端
nameSocket ns = new nameSocket();
ns.setSocket(serverSocket.accept());
//启动注册线程
new Thread(new login(ns)).start();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
注册线程:线程会给客户端进行一次交互,询问姓名,得到客户端输入的姓名后将该客户端进行保存,线程执行完毕后会自然销毁。
注册线程代码
class login implements Runnable{
//包装的客户端
private nameSocket ns;
//用于接收客户端姓名
private String str = null;
public login(nameSocket ns) {
this.ns = ns;
}
@Override
public void run() {
synchronized (Server.class){
try {
//向客户端发送信息
OutputStream outputStream = ns.getSocket().getOutputStream();
outputStream.write("来自服务器的消息:请输入您的用户名!".getBytes());
//客户端时读取一行数据,需要发送"\n"结束
outputStream.write("\n".getBytes());
outputStream.flush();
//读取客户端发送的消息
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(ns.getSocket().getInputStream()));
if((str = bufferedReader.readLine())!=null){
ns.setName(str);
Server.names.add(str);
Server.socketMap.put(str,ns);
}
outputStream.write(("来自服务器的消息:欢迎您"+str).getBytes());
outputStream.write("\n".getBytes());
outputStream.flush();
} catch (Exception e) {
if(e.getClass() == SocketException.class){
//如果异常异常类型匹配,代表客户端断开连接。
if(str!=null){
//有名字则删除客户端
System.out.println(str+"下线了!");
Server.names.remove(str);
Server.socketMap.remove(str);
}else {
System.out.println("不知名的用户下线了!");
}
}else {
e.printStackTrace();
}
}
}
}
}
轮询线程:线程负责循环读取map集合中的客户端,如果客户端有消息,将启用一个聊天线程负责输出。
轮询线程代码
class lunxun implements Runnable{
//正在轮询到的客户端姓名
private String na;
private int num = 0;
@Override
public void run() {
while(true){
try {
//加上锁
synchronized (Server.class){
//用变量num的增长实现轮询。
for (int i = 0;i<Server.names.size();i++){
na = Server.names.get(num%Server.names.size());
if(num<Server.names.size()){
num++;
}else {
num = 1;
}
//给客户端设置超时时间,出了异常会跳出循环
Server.socketMap.get(na).getSocket().setSoTimeout(10);
InputStreamReader inputStreamReader = new InputStreamReader(Server.socketMap.get(na).getSocket().getInputStream());
//约定在发送的数据前多加一个字符,保证数据的完整性。
if (inputStreamReader.read()!=-1){
new Thread(new liaotian(na,new BufferedReader(inputStreamReader))).start();
}
}
}
} catch (Exception e) {
if(e.getClass() == SocketException.class){
//异常类型相等则表示客户端下线,删除客户端。
System.out.println(na+"下线了!");
Server.names.remove(na);
Server.socketMap.remove(na);
}else if (e.getClass() == SocketTimeoutException.class){
//超时异常不做处理
}else {
e.printStackTrace();
}
}
}
}
}
聊天线程:负责打印需要输出的客户端数据流,该线程在执行结束后将自然销毁。
class liaotian implements Runnable{
//客户端姓名
private String name;
//数据流
private BufferedReader bufferedReader;
public liaotian(String name, BufferedReader bufferedReader) {
this.name = name;
this.bufferedReader = bufferedReader;
}
@Override
public void run() {
try{
System.out.println(name+":"+bufferedReader.readLine());
}catch(Exception e){
e.printStackTrace();
}
}
}
客户端
在链接上服务端时,先与服务端进行一次录入姓名的交互,接着进入循环读取控制台的信息并发送至服务端。
public class khd {
public static void main(String[] args) {
try {
String str = null;
Socket socket = new Socket(InetAddress.getByName("127.0.0.1"),10086);
Scanner scanner = new Scanner(System.in);
//获取输出流
PrintStream printStream = new PrintStream(socket.getOutputStream());
//获取输入流
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//与服务器第一次交互开始------(有些臃肿)
if ((str = bufferedReader.readLine())!= null){
System.out.println(str);
}
printStream.write(scanner.nextLine().getBytes());
printStream.write("\n".getBytes());
printStream.flush();
if ((str = bufferedReader.readLine())!= null){
System.out.println(str);
}
//与服务器第一次交互结束------
while(true){
//与服务端约定在数据前加上一个字符
printStream.write(("s"+ scanner.nextLine()+"\n").getBytes());
printStream.flush();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
效果: