基于TCP协议的Java聊天小程序
一、基本思路
1.1 利用ServerSocket
和Socket
通信基本原理
Java.net包中提供了ServerSocket和Socket类来实现基于TCP的通信。利用ServerSocket可以创建服务器,利用Socket类可以创建客户端。API对这两个类描述如下:
public class ServerSocket extends Object
此类实现服务器套接字服务器套接字等待请求通过网络传入。
它基于该请求执行某些操作,然后可能向请求者返回结果。
public class Socket extends Object
此类实现客户端套接字(也可以就叫“套接字”)。套接字是两台机器间通信的端点。
客户端-服务器通信工程中,服务器调用ServerSocket
类的accept
方法阻塞监听某一端口是否来自有客户端的的请求。若有,ServerSocket
则利用accept
得到客户端的Socket
对象。客户端利用Socket的输出流向服务端发送数据,服务端则利用客户端的Socket
对象的输入流获取客户端向服务器发来的数据。服务端向客户端发送数据时也是利用客户端的Socket
对象的输出流。具体模型如下所示:
1.2 两客户端通信实现思路
首先,服务端利用ServerSocket
类的accept
方法阻塞监听某一固定端口,此处监听65532
端口。然后,创建两个客户端,客户端都向65532
端口发送消息,客户端之间通信需依靠服务端来转发消息。如下图示:
因此,创建Server类和Client类分别模拟服务器和客户端。Server类开启服务后监听65532端口,当收到一个客户端(用户1)请求时,主线程则开启一个线程处理用户1的请求。主线程继续监听65532端口,当有新的客户端(用户2)发来请求时,主线程则再开启一个线程处理用户2的请求。主线程仍然继续监听65532端口。总之,Server类主线程用来监听新用户的请求,当新请求到达时则开启新线程处理该请求。客户端工作原理类似,客户端需并发处理接受和发送信息两个任务,因此,主线程用来处理发送信息相关的任务,需开启另一个线程来处理服务器发送来的消息。
服务器实现流程图
二、代码及运行结果
2.1 代码
服务端Server类
/**
* @Description TODO服务端,提供转发服务
* @version V1.0
*
*/
public class Server {
//@Fields clientsMap : 用来存储client对象的Map,以便服务器转发消息
private Map<String,ClientInServer> clientsMap
= new HashMap<String,ClientInServer>();
static int i = 1;
public static void main(String[] args) {
new Server().initServer(65532);
}
/**
* TODO(方法功能描述) 初始化服务端
* @param port 服务端要监听的端口号
* @throws IOException
*/
private void initServer(int port) {
//@Fields serverSocket :建立服务端socket服务。
ServerSocket serverSocket = null;
ClientInServer clientInServer = null;
Socket socket = null;
if (port>1024&&port<65535) {
try {
serverSocket = new ServerSocket(port);
System.out.println("服务器已开启");
} catch (BindException e) {
System.out.println("该端口已经被占用!");
} catch (IOException e) {
e.printStackTrace();
System.out.println("服务器开启异常!");
}
} else {
System.out.println("端口号需在1024-65535之间!");
}
//循环监听新用户
while (true) {
String userName = "用户"+i;
i++;
try {
//阻塞监听新用户连到服务器
socket = serverSocket.accept();
} catch (IOException e) {
e.printStackTrace();
}
//服务端的Socket
clientInServer = new ClientInServer(socket);
//clientInServer对象存入Map中
clientsMap.put(userName, clientInServer);
//开启新线程
new Thread(clientInServer).start(); }
}
/**
* @Description TODO 内部类 消息转发
* @version V1.0
*
*/
private class ClientInServer implements Runnable{
private Socket socket;
InputStream inStream = null;
DataInputStream din = null;
OutputStream outStream = null;
DataOutputStream dos = null;
boolean flag = true;
public ClientInServer(Socket socket) {
this.socket = socket;
try {
//得到客户端发送的消息
inStream = socket.getInputStream();
din = new DataInputStream(inStream);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
//某一客户端发送的消息
String message;
try {
while (flag) {
message = din.readUTF();
System.out.println(message);
toAllClients(message);
}
} catch (SocketException e) {
flag = false;
System.out.println("客户下线");
clientsMap.remove(this);
// e.printStackTrace();
} catch (EOFException e) {
flag = false;
System.out.println("客户下线");
clientsMap.remove(this);
// e.printStackTrace();
} catch (IOException e) {
flag = false;
System.out.println("接受消息失败");
clientsMap.remove(this);
e.printStackTrace();
}
if (din != null) {
try {
din.close();
} catch (IOException e) {
System.out.println("din关闭失败");
e.printStackTrace();
}
}
if (inStream != null) {
try {
inStream.close();
} catch (IOException e) {
System.out.println("din关闭失败");
e.printStackTrace();
}
}
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
System.out.println("din关闭失败");
e.printStackTrace();
}
}
}
/**
* TODO(方法功能描述) 消息分发
* @param message 要转发的消息
*/
private void toAllClients(String message) {
//遍历整个map
ClientInServer cs;
String userInfo = message;
List<ClientInServer> csList = new ArrayList<ClientInServer>();
for (String key :clientsMap.keySet() ) {
System.out.println(key+"\n");
//得到每个
cs = clientsMap.get(key);
if (cs == this) {
//cs==this 则自己是发送方,获取发送名
userInfo = key+"说:"+message;
} else {
csList.add(cs);
}
}
for (ClientInServer c:csList) {
try {
outStream = c.socket.getOutputStream();
} catch (IOException e) {
e.printStackTrace();
}
dos = new DataOutputStream(outStream);
sentMes(userInfo);
}
}
/**
* TODO(方法功能描述) 发送
* @param message
*/
private void sentMes(String message) {
try {
dos.writeUTF(message);
dos.flush();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("转发成功!");
}
}
}
客户端Client类
public class Client extends Frame {
private static final long serialVersionUID = 1L;
//@Fields textFieldContent :
private TextField textFieldContent = new TextField();
private TextArea textAreaContent = new TextArea();
private Socket socket = null;
private OutputStream out = null;
private DataOutputStream dos = null;
private InputStream in = null;
private DataInputStream dis = null;
private boolean flag = false;
/**
* TODO 开启客户端界面
* @param args
*/
public static void main(String[] args) {
new Client().init();
}
/**
* TODO(方法功能描述) 初始化界面,并为控件添加事件监听
*/
private void init() {
this.setSize(300, 300);
setLocation(250, 150);
setVisible(true);
setTitle("WeChatRoom");
// 添加控件
this.add(textAreaContent);
this.add(textFieldContent, BorderLayout.SOUTH);
textAreaContent.setFocusable(false);
pack();
// 关闭事件
addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.out.println("用户试图关闭窗口");
disconnect();
System.exit(0);
}
});
// textFieldContent添加回车事件
textFieldContent.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
onClickEnter();
}
});
// 建立连接
connect();
//为客户端接收消息开启线程
new Thread(new ReciveMessage()).start();
}
/**
* @Description TODO 用来处理服务器发来的消息
* @version V1.0
*
*/
private class ReciveMessage implements Runnable {
@Override
public void run() {
String time = new SimpleDateFormat("h:m:s").format(new Date());
flag = true;
try {
while (flag) {
String message = dis.readUTF();
textAreaContent.append(time+":\n"+message + "\n");
}
} catch (EOFException e) {
flag = false;
System.out.println("客户端已关闭");
} catch (SocketException e) {
flag = false;
System.out.println("客户端已关闭");
} catch (IOException e) {
flag = false;
System.out.println("接受消息失败");
e.printStackTrace();
}
}
}
/**
* TODO 回车发送消息
*/
private void onClickEnter() {
// 去掉首末空格
String message = textFieldContent.getText().trim();
if (message != null && !message.equals("")) {
String time = new SimpleDateFormat("h:m:s").format(new Date());
textAreaContent.append(time + "\n我说:" + message + "\n");
textFieldContent.setText("");
sendMessageToServer(message);
}
}
/**
* TODO(方法功能描述) 给服务端发送消息
* @param message 要发送的消息
*/
private void sendMessageToServer(String message) {
try {
dos.writeUTF(message);
dos.flush();
} catch (IOException e) {
System.out.println("发送消息失败");
e.printStackTrace();
}
}
/**
* TODO(方法功能描述) 客户端Socket连接服务端
*/
private void connect() {
try {
socket = new Socket("localhost", 65532);
out = socket.getOutputStream();
dos = new DataOutputStream(out);
in = socket.getInputStream();
dis = new DataInputStream(in);
} catch (UnknownHostException e) {
System.out.println("申请链接失败");
e.printStackTrace();
} catch (IOException e) {
System.out.println("申请链接失败");
e.printStackTrace();
}
}
/**
* TODO(方法功能描述) 关闭Socket及流
*/
private void disconnect() {
flag = false;
if (dos != null) {
try {
dos.close();
} catch (IOException e) {
System.out.println("dos关闭失败");
e.printStackTrace();
}
}
if (out != null) {
try {
out.close();
} catch (IOException e) {
System.out.println("dos关闭失败");
e.printStackTrace();
}
}
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
System.out.println("socket关闭失败");
e.printStackTrace();
};
}
}
}
2.2、运行结果
三、存在的问题
1.采用新线程处理客户端请求,存在线程安全问题。
2.客户端通信时,显示的时间有问题。这个问题也是由于采用了多线程。