基於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.客戶端通信時,顯示的時間有問題。這個問題也是由於採用了多線程。