1、初始版本
该版本实现:客户端可以把键盘录入的消息发送到服务端,服务端再把这个消息返回给客户端:
1.1 服务端
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class ChatServer {
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(9999);
Socket socket = server.accept();
//获取输入输出流
DataInputStream dis = new DataInputStream(socket.getInputStream());
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
//获取数
String msg = dis.readUTF();
//输出信息
dos.writeUTF("服务端接收到消息:"+msg);
dos.flush();
}
}
1.2 客户端
import java.io.*;
import java.net.Socket;
public class ChatClient {
public static void main(String[] args) throws IOException {
//创建连接服务端的Socket通道
Socket socket = new Socket("127.0.0.1",9999);
//控制台输入流封装
BufferedReader console = new BufferedReader(new InputStreamReader(System.in));
//获取输入输出流
DataInputStream dis = new DataInputStream(socket.getInputStream());
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
//循环,可以不断输入
while (true){
String msg = console.readLine();//得到键盘录入信息
dos.writeUTF(msg);//写出数据到通道中
dos.flush();
//接收数据
String msgStr = dis.readUTF();
System.out.println(msgStr);
}
}
}
上面的程序存在的问题:1 客户端和服务端发送和接收数据是顺序的,客户端必须先发送数据后接收,服务端必须先接收后发送。实际的聊天中,应该是发送和接收是无序的,所以应该是发送和接收是不同的线程。2 服务端只能处理客户端的一次请求
2、升级版本
该版本功能:客户端的发送和接收是不同的线程,客户端和服务端可以多次交换数据。
2.1 关闭流的工具类
import java.io.Closeable;
import java.io.IOException;
/**
* 关闭流的工具类
*/
public class CloseUtil {
public static void closeAll(Closeable ... io){
for(Closeable temp : io){
if(temp != null){
try {
temp.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
2.2 客户端
2.2.1 客户端发送消息线程
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
/**
* 客户端发送消息的线程类
*/
public class Send implements Runnable {
private BufferedReader console;//控制台输入流
private DataOutputStream dos;//输出流
private boolean isRunning = true;//控制线程的标识符
public Send(){
console = new BufferedReader(new InputStreamReader(System.in));//初始化控制台输入流
}
public Send(Socket socket){
this();//调用无参构造
try {
dos = new DataOutputStream(socket.getOutputStream());
} catch (IOException e) {
//e.printStackTrace();
isRunning = false;
CloseUtil.closeAll(dos,console);
}
}
//接收从控制台录入的信息的方法
public String getMsgFromConsole(){
try {
return console.readLine();
} catch (IOException e) {
// e.printStackTrace();
isRunning = false;
CloseUtil.closeAll(dos,console);
}
return "";
}
//发送消息的方法
public void send(){
String msg = getMsgFromConsole();
if(msg != null && !msg.equals("")){
//发送信息
try {
dos.writeUTF(msg);
} catch (IOException e) {
//e.printStackTrace();
isRunning = false;
CloseUtil.closeAll(dos,console);
}
}
}
@Override
public void run() {
while (isRunning){
send();//发送消息
}
}
}
2.2.2 客户端接收消息的线程
import java.io.DataInputStream;
import java.io.IOException;
import java.net.Socket;
/**
* 客户端接收消息的线程类
*/
public class Receive implements Runnable {
//输入流
private DataInputStream dis;
//控制线程
private boolean isRunning = true;
public Receive(){
}
public Receive(Socket socket){
try {
dis = new DataInputStream(socket.getInputStream());
} catch (IOException e) {
//e.printStackTrace();
isRunning = false;
CloseUtil.closeAll(dis);
}
}
//接收消息
public String receive(){
try {
return dis.readUTF();
} catch (IOException e) {
//e.printStackTrace();
isRunning = false;
CloseUtil.closeAll(dis);
}
return "";
}
@Override
public void run() {
while (isRunning){
System.out.println(receive());
}
}
}
2.2.3 客户端主程序类
import java.io.IOException;
import java.net.Socket;
/**
* 多人聊天客户端
*/
public class ChatClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1",9999);
//发送数据的通道
new Thread(new Send(socket)).start();
//接收数据的通道
new Thread(new Receive(socket)).start();
}
}
2.3 服务端
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 多人聊天服务端
*/
public class ChatServer {
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(9999);
while(true){//这个循环是可以接收多个客户端,但是一次只能处理一个,必须一个客户端断开才能处理另一个
Socket socket = server.accept();//一个accpet就是接收一个客户端
DataInputStream dis = new DataInputStream(socket.getInputStream());
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
while(true){//这个循环是处理一个客户端的多次输入输出
//接收消息
String s = dis.readUTF();
System.out.println(s);
//写出数据
dos.writeUTF("服务端接收到消息:"+s);
dos.flush();
}
}
}
}
上面程序的缺点:只能处理一个客户端和服务端的交互,不能处理多个客户端的交互。
3、群聊实现
最终版本,可以实现群聊,相当于QQ微信的群聊的简单功能。
注意:客户端的代码和之前是一样的,现在我们修改ChatServer:
3.1 ChatServer修改
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
/**
* 多人聊天服务端
* 服务端:给每个客户端的连接都创建一个线程,来处理该客户端的请求
*/
public class ChatServer {
private List<MyChannel> channelList = new ArrayList<>();//用来管理通道
public static void main(String[] args) throws IOException {
new ChatServer().start();
}
//封装一个方法调用线程类处理客户端请求
public void start() throws IOException {
ServerSocket server = new ServerSocket(9999);
while(true){//这个循环是可以接收多个客户端,但是一次只能处理一个,必须一个客户端断开才能处理另一个
Socket socket = server.accept();//一个accpet就是接收一个客户端
MyChannel myChannel = new MyChannel(socket);
channelList.add(myChannel);//把通道加入到集合中
new Thread(myChannel).start();
}
}
//创建一个内部线程类,处理客户端请求,进行信息的读写操作
class MyChannel implements Runnable {
private DataInputStream dis ;//输入流
private DataOutputStream dos;//输出流
private boolean isRunning = true;//线程控制标识符
public MyChannel(){
}
public MyChannel(Socket socket){//初始化
try {
dis = new DataInputStream(socket.getInputStream());
dos = new DataOutputStream(socket.getOutputStream());
} catch (IOException e) {
//e.printStackTrace();
isRunning = false;
CloseUtil.closeAll(dis,dos);
}
}
//接收消息的方法
private String receive(){
try {
return dis.readUTF();
} catch (IOException e) {
e.printStackTrace();
isRunning = false;
CloseUtil.closeAll(dis);
channelList.remove(this);//出异常就把当前通道从集合中移除
}
return "";
}
//发送消息的方法
private void send(String msg){
if(msg==null && msg.equals("")){
return;
}
try {
dos.writeUTF(msg);
dos.flush();
} catch (IOException e) {
e.printStackTrace();
isRunning = false;
CloseUtil.closeAll(dos);
channelList.remove(this);//出异常就把当前通道从集合中移除
}
}
@Override
public void run() {
while(isRunning){
//把数据发送到其他客户端
String msg = receive();//获取信息
//遍历,发送
for(MyChannel channel : channelList){
if(channel!=this){//不是自己才发送
channel.send(msg);
}
}
}
}
}
}
测试:开启服务端,运行多个客户端,我们可以把自己的消息发送到其他的客户端。
4、可以私聊的聊天室
上面第三个聊天版本已经可以实现群聊了,现在我们再加上下面两个功能:
1、添加上线提醒功能和欢迎功能;
2、添加私聊功能,约定:当客户端输入@客户端名称:内容 表示的是私聊。
4.1 客户端代码
4.1.1 发送消息线程
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
/**
* 客户端发送消息的线程类
*/
public class Send implements Runnable {
private BufferedReader console;//控制台输入流
private DataOutputStream dos;//输出流
private boolean isRunning = true;//控制线程的标识符
private String name;//当前客户端名称
public Send(){
console = new BufferedReader(new InputStreamReader(System.in));//初始化控制台输入流
}
public Send(Socket socket,String name){
this();//调用无参构造
this.name = name;
try {
dos = new DataOutputStream(socket.getOutputStream());
//创建完就立马发送名称给服务端
send(this.name);
} catch (IOException e) {
//e.printStackTrace();
isRunning = false;
CloseUtil.closeAll(dos,console);
}
}
//接收从控制台录入的信息的方法
public String getMsgFromConsole(){
try {
return console.readLine();
} catch (IOException e) {
// e.printStackTrace();
isRunning = false;
CloseUtil.closeAll(dos,console);
}
return "";
}
//发送消息的方法
public void send(String msg){
if(msg != null && !msg.equals("")){
//发送信息
try {
dos.writeUTF(msg);
} catch (IOException e) {
//e.printStackTrace();
isRunning = false;
CloseUtil.closeAll(dos,console);
}
}
}
@Override
public void run() {
while (isRunning){
String msg = getMsgFromConsole();
send(msg);//发送消息
}
}
}
4.1.2 接收消息线程
import java.io.DataInputStream;
import java.io.IOException;
import java.net.Socket;
/**
* 客户端接收消息的线程类
*/
public class Receive implements Runnable {
//输入流
private DataInputStream dis;
//控制线程
private boolean isRunning = true;
public Receive(){
}
public Receive(Socket socket){
try {
dis = new DataInputStream(socket.getInputStream());
} catch (IOException e) {
//e.printStackTrace();
isRunning = false;
CloseUtil.closeAll(dis);
}
}
//接收消息
public String receive(){
try {
return dis.readUTF();
} catch (IOException e) {
//e.printStackTrace();
isRunning = false;
CloseUtil.closeAll(dis);
}
return "";
}
@Override
public void run() {
while (isRunning){
System.out.println(receive());
}
}
}
4.1.3 客户端主程序
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
/**
* 多人聊天客户端
*/
public class ChatClient {
public static void main(String[] args) throws IOException {
//控制台输入流,获取当前客户端的名称
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
System.out.println("请输入你的名称:");
String name = bufferedReader.readLine();
Socket socket = new Socket("127.0.0.1",9999);
//发送数据的通道
new Thread(new Send(socket,name)).start();
//接收数据的通道
new Thread(new Receive(socket)).start();
}
}
4.2 服务端代码
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
/**
* 多人聊天服务端
* 服务端:给每个客户端的连接都创建一个线程,来处理该客户端的请求
*/
public class ChatServer {
private List<MyChannel> channelList = new ArrayList<>();//用来管理通道
public static void main(String[] args) throws IOException {
new ChatServer().start();
}
//封装一个方法调用线程类处理客户端请求
public void start() throws IOException {
ServerSocket server = new ServerSocket(9999);
while(true){//这个循环是可以接收多个客户端,但是一次只能处理一个,必须一个客户端断开才能处理另一个
Socket socket = server.accept();//一个accpet就是接收一个客户端
MyChannel myChannel = new MyChannel(socket);
channelList.add(myChannel);//把通道加入到集合中
new Thread(myChannel).start();
}
}
//创建一个内部线程类,处理客户端请求,进行信息的读写操作
class MyChannel implements Runnable {
private DataInputStream dis ;//输入流
private DataOutputStream dos;//输出流
private boolean isRunning = true;//线程控制标识符
private String name;//连接的客户端名称
public MyChannel(){
}
public MyChannel(Socket socket){//初始化
try {
dis = new DataInputStream(socket.getInputStream());
dos = new DataOutputStream(socket.getOutputStream());
this.name = dis.readUTF();//得到客户端发送的名称
//在创建完服务客户端的线程,就提示欢迎信息
send(this.name+",您好,欢迎进入聊天室");
//然后把当前客户端名称发送给其他客户端
sendToOthers(this.name+" 进入了聊天室");
} catch (IOException e) {
//e.printStackTrace();
isRunning = false;
CloseUtil.closeAll(dis,dos);
}
}
//接收消息的方法
private String receive(){
try {
return dis.readUTF();
} catch (IOException e) {
e.printStackTrace();
isRunning = false;
CloseUtil.closeAll(dis);
channelList.remove(this);//出异常就把当前通道从集合中移除
}
return "";
}
//发送消息的方法
private void send(String msg){
if(msg==null && msg.equals("")){
return;
}
try {
dos.writeUTF(msg);
dos.flush();
} catch (IOException e) {
e.printStackTrace();
isRunning = false;
CloseUtil.closeAll(dos);
channelList.remove(this);//出异常就把当前通道从集合中移除
}
}
//发送消息给其他客户端的方法
private void sendToOthers(String msg){
//判断消息是私聊消息还是群聊消息,如果以@开头的就是私聊
if(msg.startsWith("@") && msg.indexOf(":")>0){
//说明的私聊消息。得到私聊的客户端名称
String name = msg.substring(1,msg.indexOf(":"));//私聊的客户端名称
String msgStr = msg.substring(msg.indexOf(":")+1);//客户端发来的消息
for(MyChannel channel : channelList){
if(channel.name.equals(name)){
channel.send(msgStr);
}
}
}else{
//把数据发送到其他客户端
for(MyChannel channel : channelList){
if(channel!=this){//不是自己才发送
channel.send(msg);
}
}
}
}
@Override
public void run() {
while(isRunning){
String msg = receive();//获取信息
sendToOthers(msg);
}
}
}
}