socket就是指两个应用程序之间通信的抽象对象,我们可以使用socket实现网络应用程序。例如一个多人聊天室。
目录
先从服务端开始
创建一个窗口类
创建一些方法,用于管理服务端链接,或者进行消息的发送。
编写一个多线程类,用于监听用户的消息输入
回到服务端窗口类,添加一些变量
编写addbutton方法
编写runnable方法
服务端窗体类添加一个构造方法
SendMessageToHost方法
dispose方法
服务端预览图
客户端的制作
addbutton方法
dispose方法
connect方法
登录窗口类
addbutton方法
addlistener方法
构造方法
预览
客户端的构造方法
预览一下客户端
最终效果图
先从服务端开始
创建一个窗口类
我习惯在主包下创建内部类,大家也可以另外自己创建一个包或者类。
public class Main
{
public static void main(String[] args)
{
new serverFrame();
}
public static class serverFrame extends JFrame
{
}
}
创建一些方法,用于管理服务端链接,或者进行消息的发送。
//等会调用这个给窗口添加组件
private void addButton(){}
//等会调用这个方法来唤起多线程
private void runnable(){}
//在监听客户端发送信息的线程中调用此方法,向服务端输出接收到的信息,同时向其他客户端输出。
public void SendMessageToHost(String message,Socket s){}
@override
public void dispose(){super.dispose();}
编写一个多线程类,用于监听用户的消息输入
当用户链接至服务器时,首先单独创建一个多线程,用于监听该用户的字节流,如果有数据,则调用服务端的SendMessageToHost方法。
//监听客户端向服务端发送信息的线程
public class MessageListener extends Thread {
//客户端的字节流
public BufferedReader reader;
//如果客户端发送消息,通过此对象调用SendMessageToHost方法,向服务器发送信息。
public Main.serverFrame server;
//代表客户端自己的socket
public Socket self;
public MessageListener(BufferedReader r, Main.serverFrame s, Socket self)
{
reader = r;
server = s;
this.self = self;
}
@Override
public void run() {
while (true)
{
try {
String message = reader.readLine();
server.SendMessageToHost(message,self);
if (message.equals("exit"))
{
self.close();
break;
}
}
catch (IOException ex)
{
ex.printStackTrace();
break;
}
}
}
}
回到服务端窗口类,添加一些变量
用于表示我们一会将会用到的一些数据。
private ServerSocket server;
//用来记录所有用户的信息输出流
private Map<Socket, MessageListener> users=new HashMap<>();
//用来记录所有用户的信息输入流
private Map<Socket, PrintWriter> socketInputStream = new HashMap<>();
//用来记录所有用户的链接socket
private List<Socket> sockets = new ArrayList<>();
//客户端向服务端发送信息的显示的地方
private JTextArea conMessage=new JTextArea();
private JScrollPane panel=new JScrollPane(conMessage);
//服务端向客户端发送信息的地方
private JTextField inputArea = new JTextField();
以上是我们服务端窗口类内的全部变量,一会我们都会用得到的~
server就代表我们自己的socket;
users用来根据用户的socket获取相应的监听线程对象,当发生意外或任何情况时,可以进行一个关闭之类的管理操作;
socketInputStream用来获取客户端的输入流,方便服务端发送消息。
接下来我们开始逐步编写窗口类的方法体,首先
编写addbutton方法
,对窗体的一些控件进行布置。
private void addButton()
{
conMessage.setFont(new Font(null,Font.PLAIN,30));
conMessage.setEditable(false);
getContentPane().add(panel,BorderLayout.CENTER);
inputArea.setFont(new Font(null,Font.PLAIN,30));
//添加文本域回车事件(发送信息)
inputArea.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
if (!inputArea.getText().equals(""))
{
conMessage.append("你:" + inputArea.getText() + "\n");
if (users.size() != 0)
{
//向所有客户端的信息输入流传入信息
for (Socket s : socketInputStream.keySet())
{
socketInputStream.get(s).println("主机:" + inputArea.getText());
}
}
inputArea.setText("");
}
}
});
getContentPane().add(inputArea,"South");
}
接下来再
编写runnable方法
,在里边写一个继承thread的匿名内部类,持续监听客户端的接入。
private void runnable()
{
//监听客户端链接线程
new Thread()
{
@Override
public void run()
{
try {
while (true)
{
Socket socket = server.accept();
if (!users.containsKey(socket))
{
//客户端链接数量阈值
if (users.size() < 5)
{
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
MessageListener listener = new MessageListener(reader,serverFrame.this,socket);
listener.start();
users.put(socket,listener);
socketInputStream.put(socket,new PrintWriter(socket.getOutputStream(),true));
sockets.add(socket);
conMessage.append("客户端连接,ip:" + socket.getInetAddress() + "\n");
conMessage.append("现在聊天室人数:" + (1 + users.size()) + "\n");
for (Socket s : socketInputStream.keySet())
{
socketInputStream.get(s).println("客户端连接,ip:" + socket.getInetAddress());
socketInputStream.get(s).println("现在聊天室人数:" + (1 + users.size()));
}
}
else
{
System.out.println("客户端接入数量已满!已拒绝一名接入!");
conMessage.append("客户端接入数量已满!已拒绝一名接入!");
socket.close();
}
}
}
}
catch (IOException ex)
{
JOptionPane.showMessageDialog(serverFrame.this,"错误!" + ex.getMessage());
ex.printStackTrace();
for (Socket s : socketInputStream.keySet())
{
socketInputStream.get(s).println("服务器出现错误,连接终止。");
socketInputStream.remove(s);
sockets.remove(s);
users.remove(s);
try {
s.close();
}
catch (IOException ex2)
{
ex2.printStackTrace();
}
}
dispose();
}
}
}.start();
}
这两个方法写完之后,给
服务端窗体类添加一个构造方法
,在初始化的时候调用这两个方法,顺便对窗体的一些属性进行设置
public serverFrame()
{
setTitle("socket服务器");
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
setBounds(500,400,800,600);
addButton();
try {
server=new ServerSocket(8390);
}
catch (IOException ex)
{
JOptionPane.showMessageDialog(this,"错误!" + ex.getMessage());
ex.printStackTrace();
}
runnable();
setVisible(true);
}
然后再来编写一下我们的
SendMessageToHost方法
,这个方法的用处前面已经说过了~
//在监听客户端发送信息的线程中调用此方法,向服务端输出接收到的信息,同时向其他客户端输出。
public void SendMessageToHost(String message,Socket s)
{
//如果信息是exit则客户端退出
if (message.equals("exit"))
{
users.remove(s);
sockets.remove(s);
socketInputStream.remove(s);
conMessage.append("客户端:" + s.getInetAddress() + " 已退出");
conMessage.append("现在聊天室人数:" + (1 + users.size()));
if (users.size() != 0)
{
for (Socket s_2 : socketInputStream.keySet())
{
socketInputStream.get(s_2).println("客户端:" + s.getInetAddress() + " 已退出");
socketInputStream.get(s_2).println("现在聊天室人数:" + (1 + users.size()));
}
}
return;
}
else
{
conMessage.append("\n" + message);
for (Socket s_2 : socketInputStream.keySet())
{
if (!s_2.equals(s))
{
socketInputStream.get(s_2).println(message);
}
}
}
}
最后再来编写一下
dispose方法
,保证服务端窗口关闭后,与服务端链接的socket也能正常关闭。
@Override
public void dispose()
{
try {
server.close();
for (Socket s : sockets)
{
users.remove(s);
socketInputStream.remove(s);
sockets.remove(s);
s.close();
}
}
catch (IOException ex)
{
ex.printStackTrace();
}
super.dispose();
}
至此,整个服务端程序已经编写完成,
服务端预览图
打开后是这个样子的:
做的比较简便,大家可以根据自己的喜好或者想法去修改这个窗体的布局
下面就可以进入到
客户端的制作
了,客户端只做起来相对比较简单。
同样我还是选择在主包进行编写,把客户端窗口类写成一个主包的内部类
public class Main
{
public static void main(String[] args)
{
new ClientFrame();
}
public static class ClientFrame extends JFame
{
}
}
老样子,先往客户端窗口类里添加一些一会将会用到的数据。
//在聊天室中展示的名字
public String userName="";
//服务器套接字
private Socket server;
//客户端输出流
private PrintWriter serverOutputStream;
//客户端输入流
private BufferedReader serverInputStream;
//显示信息的地方
private JTextArea conMessage=new JTextArea();
private JScrollPane panel=new JScrollPane(conMessage);
private JTextField inputArea = new JTextField();
serverOutputStream用来等会向服务器发送字节流。
serverInputStream用来接收服务器的字节流。
inputArea用来输入客户端向服务端发送的消息。
然后我们给客户端添加一个
addbutton方法
,像服务端那样,addbutton方法用于对窗体进行布局
private void addbutton()
{
conMessage.setEditable(false);
conMessage.setFont(new Font(null,Font.PLAIN,30));
getContentPane().add(panel,BorderLayout.CENTER);
inputArea.setFont(new Font(null,Font.PLAIN,30));
//添加文本输入框监听事件
inputArea.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
//如果没连接上服务器
if (server == null)
{
int value = JOptionPane.showConfirmDialog(ClientFrame.this,"服务器未能连接成功,是否重新连接?","提示",JOptionPane.YES_NO_OPTION);
if (value == 0)
{
new ConnectDialog(ClientFrame.this);
}
}
//否则向服务器发送信息
else
{
serverOutputStream.println(userName + ":" + inputArea.getText());
conMessage.append("我:" + inputArea.getText() + "\n");
inputArea.setText("");
}
}
});
getContentPane().add(inputArea,"South");
}
然后编写
dispose方法
用来保证程序关闭的同时与服务端断开连接
@Override
public void dispose() {
if (server != null)
{
serverOutputStream.println("exit");
try {
server.close();
serverInputStream.close();
serverOutputStream.close();
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
super.dispose();
}
现在添加
connect方法
,调用此方法向服务端聊天室发起连接。
//调用此方法向服务器发起链接
public void connect(String name, String ip,String port)
{
this.userName = name;
try {
server = new Socket(ip,Integer.parseInt(port));
serverOutputStream = new PrintWriter(server.getOutputStream(),true);
}
catch (UnknownHostException ex1)
{
JOptionPane.showMessageDialog(this,"连接错误!" + ex1.getMessage());
ex1.printStackTrace();
}
catch (IOException ex)
{
JOptionPane.showMessageDialog(this,"连接错误!" + ex.getMessage());
ex.printStackTrace();
}
if (server!=null)
{
try {
serverInputStream = new BufferedReader(new InputStreamReader(server.getInputStream()));
new Thread()
{
@Override
public void run() {
//持续监听服务端消息
while (true)
{
try {
String message = serverInputStream.readLine();
conMessage.append(message + "\n");
}
catch (IOException ex)
{
JOptionPane.showMessageDialog(ClientFrame.this,"读取信息错误!" + ex.getMessage());
ex.printStackTrace();
}
}
}
}.start();
}
catch (IOException ex)
{
JOptionPane.showMessageDialog(this,"创建输入流错误!" + ex.getMessage());
ex.printStackTrace();
}
}
}
客户端有了连接方法,但是我们还需要在指定的地方调用他,所以创建一个
登录窗口类
//链接服务器窗口
private static class ConnectDialog extends JDialog
{
}
添加按钮和文本输入,还有一些变量
//通过这个对象调用链接方法
private ClientFrame main;
private JLabel nameLabel=new JLabel("昵称:",SwingConstants.CENTER);
private JTextField nameTextField=new JTextField();
private JLabel serveripAddress=new JLabel("主机:",SwingConstants.CENTER);
private JTextField AddressTextfield=new JTextField();
private JLabel AddressPort=new JLabel("端口:",SwingConstants.CENTER);
private JTextField portTextfield=new JTextField();
private JButton JbuttonConnect = new JButton("连接");
private JButton jbuttonCancel = new JButton("取消");
addbutton方法
用来添加按钮布局
private void addbutton()
{
nameLabel.setBounds(20,30,100,40);
nameLabel.setFont(new Font(null,Font.PLAIN,30));
add(nameLabel);
nameTextField.setBounds(110,30,230,50);
nameTextField.setFont(new Font(null,Font.PLAIN,27));
add(nameTextField);
serveripAddress.setBounds(20,100,100,40);
serveripAddress.setFont(new Font(null,Font.PLAIN,30));
add(serveripAddress);
AddressTextfield.setBounds(110,100,230,50);
AddressTextfield.setFont(new Font(null,Font.PLAIN,27));
add(AddressTextfield);
AddressPort.setBounds(20,170,100,40);
AddressPort.setFont(new Font(null,Font.PLAIN,30));
add(AddressPort);
portTextfield.setBounds(110,170,230,50);
portTextfield.setFont(new Font(null,Font.PLAIN,27));
add(portTextfield);
JbuttonConnect.setBounds(80,240,100,40);
JbuttonConnect.setFont(new Font(null,Font.PLAIN,30));
add(JbuttonConnect);
jbuttonCancel.setBounds(215,240,100,40);
jbuttonCancel.setFont(new Font(null,Font.PLAIN,30));
add(jbuttonCancel);
}
addlistener方法
用于添加按钮监听以及输入的信息格式检查,这里我随便弄了下,检查不是很严格。
private void addListener()
{
//简单的进行一些数据检验
JbuttonConnect.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (nameTextField.getText().length() > 8)
{
JOptionPane.showMessageDialog(ConnectDialog.this,"你的名字太长了!");
return;
}
if (AddressTextfield.getText().length() > 15)
{
JOptionPane.showMessageDialog(ConnectDialog.this,"ip地址不合法!");
return;
}
if (portTextfield.getText().length() > 5)
{
JOptionPane.showMessageDialog(ConnectDialog.this,"端口数值不合法!");
return;
}
//检查通过,调用链接方法向服务端发起链接
main.connect(nameTextField.getText(),AddressTextfield.getText(),portTextfield.getText());
dispose();
}
});
jbuttonCancel.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
dispose();
}
});
}
构造方法
public ConnectDialog(JFrame frame)
{
super(frame,"连接窗口",true);
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
this.main = (ClientFrame) frame;
setBounds(700,600,400,350);
setLayout(null);
addbutton();
addListener();
setVisible(true);
}
至此登录窗口已经设计完成,可以打开
预览
最后回到
客户端的构造方法
public ClientFrame()
{
setTitle("socket连接端");
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
setBounds(500,400,800,600);
addbutton();
setVisible(true);
new ConnectDialog(this);
}
至此,,客户端也已经完成制作,一起来
预览一下客户端
吧。
其实我设计的样式是跟服务端一样的~~
现在打包这两个程序出来,测试一下吧!
最终效果图