您的位置:首页 > 理论基础 > 计算机网络

基于TCP协议的Java聊天小程序

2017-08-28 14:50 330 查看

基于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类主线程用来监听新用户的请求,当新请求到达时则开启新线程处理该请求。客户端工作原理类似,客户端需并发处理接受和发送信息两个任务,因此,主线程用来处理发送信息相关的任务,需开启另一个线程来处理服务器发送来的消息。

服务器实现流程图

Created with Raphaël 2.1.0开始监听端口65532accept 阻塞端口有新请求?开启新线程处理请求yesno

二、代码及运行结果

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.客户端通信时,显示的时间有问题。这个问题也是由于采用了多线程。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  java socket tcp 聊天