这阵子在学习网络编程,为了把学到的知识能运用起来,就写了个聊天室小程序,期间过程颇为曲折。其实聊天室代码很好实现,所以很快就写完了,一个偶然的机会看见我一个同学用C++实现了一个有图形界面的聊天室,刚好上学期学校有Java的GUI课程所以就想自己也写一个图形界面聊天室 (哈哈~),下面开始正文啦
第一步(实现单线程版聊天室)
相信很多童鞋都和我一样,如果代码一多就很难快速的理顺程序的思路,所以先从简单的开始看,脑子里先有个大体的实现框架单线程版聊天室(代码)
知识点
IP地址:在网络中找到一台设备
端口号:唯一标识本台机器中的一个应用设备
所以有用这两个做标识就可以找到唯一的一个端口来进行聊天。
Socket通信:
1.先建立连接:知道目标服务器的IP地址和端口号
2.获取本连接中的输入、输出流
3.通过IO进行数据的读取与写入
4.关闭流
实现过程
代码实现
客户端类:
package hhh.Test;
import java.io.IOException;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;
public class sigleClient {
public static void main(String[] args)throws IOException {
Socket cilent=null;
Scanner in=null;
PrintStream out=null;
try {
//与服务端建立连接
cilent=new Socket("127.0.0.1",6666);
//获取客户端输入输出流
out=new PrintStream(cilent.getOutputStream(),true,"UTF-8");
in=new Scanner(cilent.getInputStream());
//向服务端发送信息
out.println("Hello Server,I am cilent!");
//读取服务端发来的信息
if(in.hasNext()){
System.out.println("服务器说:"+in.nextLine());
}
} catch (IOException e) {
e.printStackTrace();
}finally {
//关闭流
cilent.close();
in.close();
out.close();
}
}
}
服务端类:
package hhh.Test;
import java.io.IOException;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class sigleServer {
public static void main(String[] args)throws IOException {
ServerSocket server=null;
Socket cilent=null;
Scanner cilentIn=null;
PrintStream cilentOut=null;
try {
//建立基站
server = new ServerSocket(6666);
//等待客户端的连接,返回客户端Socket
cilent=server.accept();
//获取客户端的输入输出流
cilentIn=new Scanner(cilent.getInputStream());
cilentOut=new PrintStream(cilent.getOutputStream(),true,"UTF-8");
//读取客户端发来的消息
if(cilentIn.hasNext()){
System.out.println("客户端说:"+cilentIn.nextLine());
}
//向客户端发消息
cilentOut.println("Hello Client!,I am Server");
} catch (IOException e) {
e.printStackTrace();
}finally {
server.close();
cilentIn.close();
cilentOut.close();
}
}
}
运行结果
第二步(实现多线程版聊天室)
看完单线程聊天室之后,了解到服务器其实就是一个中转站,将在一个客户端发来的消息转发给另一个客户端,而客户端只需要向服务端发和接收消息。但是单线程通信有很多弊端:
1.容易造成双方卡死的现象(类比电话占线)
2.发送一次数据后服务器与客户端均退出,不能持久通信
3.不能同时进行数据的读取与数据的写入,顺序操作(多线程可读写分离,作为两个不相关的线程)
4.服务器只能处理一个客户端的连接(多线程可以每当有一个客户端的连接进来就创建一个线程处理此客户端请求)
实现流程
代码实现
客户端类
package hhh.Test.MyThread;
import java.io.IOException;
import java.io.PrintStream;
import java.net.InetAddress;
import java.net.Socket;
import java.util.Scanner;
/**
* 读取服务器信息线程
*/
class ReadFromServerThread implements Runnable {
private Socket client;
public ReadFromServerThread(Socket client) {
this.client = client;
}
@Override
public void run() {
try {
//获取客户端输入流
Scanner in=new Scanner(client.getInputStream());
in.useDelimiter("\n");
while(true){
if(in.hasNext()){
System.out.println("从服务器发来的消息为:"+in.next());//碰到空格结束
}
if(client.isClosed()){
System.out.println("客户端已经关闭");
break;
}
}
in.close();
} catch (IOException e) {
System.err.println("客户端读线程异常,错误为 "+e);
}
}
}
/**
* 将信息发送给服务器线程
*/
class WriteToServerThread implements Runnable {
private Socket client;
public WriteToServerThread(Socket client) {
this.client = client;
}
@Override
public void run() {
try {
//从键盘中输入
Scanner cin=new Scanner(System.in);
cin.useDelimiter("\n");
//获取客户端输出流
PrintStream out=new PrintStream(client.getOutputStream());
while(true){
System.out.println("请输入要发送的信息..");
String msg;
//只能读一行信息
if(cin.hasNext()){
msg=cin.nextLine().trim();//读取信息,删除信息的收尾空白
out.println(msg);
if(msg.equals("byebye")){
System.out.println("关闭客户端");
cin.close();
out.close();
client.close();
break;
}
}
}
} catch (IOException e) {
System.out.println("客户端写线程异常,错误为 "+e);
}
}
}
public class threadClient{
public static void main(String[] args) {
try {
//连接服务器
Socket client=new Socket(InetAddress.getLocalHost(),6666);
//读取服务器消息
Thread readFromServer = new Thread(new
ReadFromServerThread(client));
//向服务器发送消息
Thread writeToServer = new Thread(new
WriteToServerThread(client));
readFromServer.start();
writeToServer.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
服务器类
package hhh.Test.MyThread;
import java.io.IOException;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Iterator;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 多线程聊天室服务端
* @author yuisama
*/
public class threadServer {
// 存储所有注册的客户端
private static Map<String, Socket> clientMap = new
ConcurrentHashMap<String, Socket>();
// 具体处理与每个客户端通信的内部类
private static class ExecuteClient implements Runnable {
private Socket client;
public ExecuteClient(Socket client) {
this.client = client;
}
@Override
public void run() {
try {
// 获取客户端输⼊流
Scanner in = new Scanner(client.getInputStream());
String strFromClient;
while (true) {
if (in.hasNextLine()) {
strFromClient = in.nextLine();
// windows下将默认换⾏/r/n中的/r替换为空字符串
Pattern pattern = Pattern.compile("\r");
Matcher matcher = pattern.matcher(strFromClient);
strFromClient = matcher.replaceAll("");
// 注册流程
if (strFromClient.startsWith("userName")) {
String userName = strFromClient.split("\\:")[1];
registerUser(userName,client);
continue;
}
// 群聊流程
if (strFromClient.startsWith("G")) {
String msg = strFromClient.split("\\:")[1];
groupChat(msg);
continue;
}
// 私聊流程
if (strFromClient.startsWith("P")) {
String userName = strFromClient.split("\\:")[1]
.split("-")[0];
String msg = strFromClient.split("\\:")[1]
.split("-")[1];
privateChat(userName,msg);
}
// ⽤户退出
if (strFromClient.contains("byebye")) {
String userName = null;
// 根据Socket找到UserName
for (String keyName : clientMap.keySet()) {
if (clientMap.get(keyName).equals(client)) {
userName = keyName;
}
}
System.out.println("⽤户"+userName+"下线了!");
clientMap.remove(userName);
continue;
}
}
}
}catch (IOException e) {
System.err.println("服务器通信异常,错误为 "+e);
}
}
// 注册⽅法
private void registerUser(String userName,Socket client) {
System.out.println("⽤户姓名为: "+userName);
System.out.println("⽤户"+userName+"上线了!");
System.out.println("当前群聊⼈数为: "+(clientMap.size()+1)+"⼈");
// 将⽤户信息保存到map中
clientMap.put(userName,client);
try {
PrintStream out = new PrintStream(client.getOutputStream(),
true,"UTF-8");
// 告知⽤户注册成功
out.println("⽤户注册成功!");
} catch (IOException e) {
e.printStackTrace();
}
}
// 群聊流程
private void groupChat(String msg) {
// 取出clientMap中所有Entry遍历发送群聊信息
Set<Map.Entry<String,Socket>> clientSet = clientMap.entrySet();
for (Map.Entry<String,Socket> entry : clientSet) {
try {
Socket socket = entry.getValue();
// 取得每个客户端的输出流
PrintStream out = new
PrintStream(socket.getOutputStream(),
true,"UTF-8");
out.println("群聊信息为: "+msg);
}catch (IOException e) {
System.err.println("群聊异常,错误为 "+e);
}
}
}
// 私聊流程
private void privateChat(String userName,String msg) {
Socket privateSocket = clientMap.get(userName);
try {
PrintStream out = new
PrintStream(privateSocket.getOutputStream(),
true,"UTF-8");
out.println("私聊信息为: "+msg);
}catch (IOException e) {
System.err.println("私聊异常,错误为"+e);
}
}
}
public static void main(String[] args) throws Exception{
ExecutorService executorService = Executors.newFixedThreadPool(20);
ServerSocket serverSocket = new ServerSocket(6666);
for (int i = 0 ; i < 20 ; i++) {
System.out.println("等待客户端连接...");
Socket client = serverSocket.accept();
System.out.println("有新的客户端连接,端⼝号为: "+client.getPort());
executorService.submit(new ExecuteClient(client));
}
executorService.shutdown();
serverSocket.close();
}
}
第三步(图形界面多线程版聊天室)
图形界面多线程版聊天室,就是在多线程下加了个图形界面类,基本实现和上面大体思路是一至的,在这里不在赘述。
运行结果:
群发示例:
私法示例: