计算机网络实验二:UDP套接字编程实现多人聊天

一、实验目的

1. 实现一个能够在局域网中进行点对点聊天的实用程序。

2. 熟悉c++、Java等高级编程语言网络编程的基本操作。

3. 基本了解对话框应用程序的编写过程。

4. 实现UDP套接字编程。

 

二、实验内容

(一)实验思路

1、学习理解UDP协议。

2、实现UDP客户端与服务器之间的通信。

3、实现UDP客户端之间多线程通信以及聊天页面的UI实现。

 

(二)实验步骤

1、理解UDP协议

UDP 是User Datagram Protocol的简称,中文名是用户数据报协议,是一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。UDP用来支持那些需要在计算机之间传输数据的网络应用。包括网络视频会议系统在内的众多的客户/服务器模式的网络应用都需要使用UDP协议。UDP协议的主要作用是将网络数据流量压缩成数据包的形式。一个典型的数据包就是一个二进制数据的传输单位。每一个数据包的前8个字节用来包含报头信息,剩余字节则用来包含具体的传输数据。

 

2、实现UDP客户端与服务器之间的简单通信。

  首先利用UDP实现客户端发送消息,服务器端接收消息,利用控制台接受传递消息。以exit为退出标志。要创建两个类,一个服务器一个客户端。运行时先运行服务器使其处于接收客户端消息的阻塞状态,然后再运行客户端,向服务器发送消息。

(1)服务器端:

主要事件:创建DatagramSocket对象,用于打开指定端口并监听,然后创建一个DatagramPacket,利用DatagramSocket中的receive(ds)方法接收数据并放到刚创建的DatagramPacket中,然后响应客户端。

 具体实现:

① 首先需要导入java.net.*,java.io.*以及打印聊天时间所需要的java.util.Date。

import java.net.* ;

import java.util.Date;

import java.io.* ;

 

② 然后写Server类。先创建服务器端套接字DatagramSocket并且指定端口,然后创建一个字节数组用来接收客户端发送的数据,再创建一个DatagramPacket用来接收数据,一个DatagramPacket用来发送数据。创建一个BufferReader用来读取键盘输入。

public class Server

{

public static void main(String[] args)throws Exception{

//创建服务器端DatagramSocket,指定端口

   DatagramSocket ds = new DatagramSocket(9999);

   //创建数据报,以接收客户端发送的数据

   byte[] buf = new byte[1024];//创建字节数组,指定接收的数据报的大小

   DatagramPacket dpreceive = new DatagramPacket(buf,buf.length);

   String str = null ;

   String strSend = null ;

   BufferedReader br = new BufferedReader(new InputStreamReader(System.in));//获取键盘输入

       byte[] bufSend = new byte[1024];

       //创建数据报,包含响应的数据

   DatagramPacket dpsend = null ;

 

③ 当收到的不为客户端退出消息时,服务器利用receive方法接收客户端的数据报,该方法在接收到数据报前一直处于阻塞状态。接收到后在控制台打印出来。

while(!"exit".equals(str)){

   //接收报文

   ds.receive(dpreceive) ;//此方法在接收到数据报之前会一直阻塞

   str = new String(buf,0,dpreceive.getLength()) ;

   System.out.println("【client】"+new Date()+"  "+str) ;//若显示则表示已收到客户机发来的数据报


④ 服务器端向客户端发送数据,读取键盘输入后保存到DatagramPacket中,利用getSocketAddress()方法获取客户端的地址和端口号,然后发送响应报文。

  System.out.print("【server】") ;

   strSend = br.readLine() ;

   bufSend = strSend.getBytes() ;

   dpsend = new DatagramPacket(bufSend,bufSend.length,dpreceive.getSocketAddress()) ;//IP+PORT

   //响应客户端

   ds.send(dpsend) ;  

       }ds.close();

}

};

 

(2)客户端:

创建DatagramSocket对象,创建好IP地址和端口号后,利用DatagramSocket中的connect(ip , port)方法和服务端建立连接,然后利用DatagramSocket中的send(dp)方法发送保存在数据包里的数据。

 具体实现:

① 客户端类。创建一个DatagramPacket来存放要发送的内容,在创建一个用来存放接收到的内容。创建客户端的套接字。获取键盘输入。

public class Client//向服务器端发送数据

{

    public static void main(String[] args)throws Exception{

    

   byte[] buf = new byte[1024] ; //将数据及源和目的封装成数据包中,不需要建立连接。

   byte[] bufReceive = new byte[1024] ;

   DatagramPacket dpsend = null ;//创建数据报,存放要发送的内容

   DatagramSocket ds = null ;//创建客户端DatagramSocket      BufferedReader br = null ;

   DatagramPacket dpreceive ;//创建数据报,存放要接收的内容

   String str = null ;

   String strReceive = null ;

        

   br = new BufferedReader(new InputStreamReader(System.in)) ;

   System.out.println("你已进入聊天室:") ;

   System.out.print("【client】") ;

       str = br.readLine() ;

   ds = new DatagramSocket(8888);

② 利用DatagramSocket指明地址与端口号和服务端建立连接,然后利用send方法发送保存在数据包里的数据。

 //以exit为结束聊天标志

   while (!"exit".equals(str))

   {

   buf = str.getBytes() ;

   dpsend = new DatagramPacket(buf,buf.length,new InetSocketAddress("127.0.0.1",9999)) ;

   ds.send(dpsend) ;

   dpreceive = new DatagramPacket(bufReceive,bufReceive.length) ;

           ds.receive(dpreceive) ;

           strReceive = new String(bufReceive,0,dpreceive.getLength()) ;

 

   System.out.println("【server】"+new Date()+"  "+strReceive) ;//若显示则表示已收到服务器发来的数据报

   System.out.print("【client】");

   str = br.readLine() ;

   str = str.trim();//去除字符串的首尾空格

   }

 

③ 输入结束标志时,和上述操作基本相同,输出退出标志即可,然后关闭客户端的套接字。

   //当输入exit时

   buf = str.getBytes() ;//将字符串编码为字节序列,存储到字节数组中。

   dpsend = new DatagramPacket(buf,buf.length,new InetSocketAddress("127.0.0.1",9999)) ;

   ds.send(dpsend) ;

   System.out.print("……【client】已离开聊天室 ……");

   ds.close();//关闭socket

}

};


3、实现UDP客户端之间多线程通信以及聊天页面的UI实现。

实验时首先进行了客户端和服务器之间发送消息,但是考虑到多人参与的聊天应用,是客户端与客户端进行聊天通信,客户端与客户端聊天的本质是服务器端将收到的信息转发给每一个客户端。下面利用多线程实现多个客户端之间的聊天,并且加入聊天窗口(界面实现)。

(1)服务器:

服务器只需要开启一个线程来接收客户端发送的消息,因为它不知道要有多少客户端连接,也不知道客户端什么时候需要连接。服务器接收到消息后,要把消息发送给所有的客户端,就需要知道各个客户端的IP地址和端口号,receive()方法接受到的数据中包含了这些信息,但是还需要存起来,我使用一个ArrayList来保存。

 具体实现:

① 首先需要导入以下包。

import java.awt.Font;

import java.net.DatagramPacket;

import java.net.DatagramSocket;

import java.net.SocketAddress;

import java.util.ArrayList;

import javax.swing.UIManager;

 

② 服务器类需要实现Runnable接口以开启线程。

public class UDPserver implements Runnable{

private DatagramSocket DS;

private int Port = 9998;

private ArrayList<SocketAddress> clients = new ArrayList<SocketAddress>(); //保存客户端IP地址

③ 构造方法里创建服务器端套接字并制定端口,然后开启服务器端线程。

public UDPserver()throws Exception{

try {

DS = new DatagramSocket(Port);//创建服务器端DatagramSocket,指定端口9998

new Thread(this).start();//开启服务器端线程

} catch (Exception ex) {

ex.printStackTrace();

}

}

④ 重写run方法。除了之前服务器和客户端之间的相同操作以外,这里服务器还需要保存客户端的IP和端口号。

public void run(){//重写run方法

try{

while(true){

byte[] data = new byte[1024];//创建字节数组,指定可以接收的数据报的大小

DatagramPacket DP = new DatagramPacket(data,data.length);

DS.receive(DP);//接收报文,此方法在接收到数据报之前会一直阻塞

SocketAddress clientip = DP.getSocketAddress(); //得到客户端的IP+PORT

if(!clients.contains(clientip)){//把客户端的IP+PORT存起来

clients.add(clientip);

}

this.sendAllClients(DP);//服务器向每一个客户端发送消息,以实现多人聊天。

}

}catch(Exception ex){

ex.printStackTrace();

}

}

 

⑤ 服务器向每一个客户端发送消息,以实现多人聊天。

private void sendAllClients(DatagramPacket dp) throws Exception {

for(SocketAddress addr : clients){

DatagramPacket dd = new DatagramPacket(dp.getData(),dp.getLength(),addr);

DS.send(dd);

}

}

⑥ main函数中直接new一个服务器的实例,调用其构造方法。

public static void main(String[] args) throws Exception{

new UDPserver();

}

}

 

(2)客户端:

多个客户端聊天时需要区分身份,所以每打开客户端都需要输入用户名(和端口号)。然后向服务器发送消息,再接收服务器的消息。在客户端界面中,我使用了回车发送消息,并且清空输入框。显示在屏幕上的除了所发的消息,还输出发送时间和用户名、用户IP及端口号。

具体实现:

import java.awt.BorderLayout;

import java.awt.Color;

import java.awt.Font;

import java.awt.event.ActionEvent;

import java.awt.event.ActionListener;

import java.net.DatagramPacket;

import java.net.DatagramSocket;

import java.net.InetAddress;

import java.text.*;

import java.util.Date;

 

import javax.swing.*;


① 客户端需要继承窗体JFrame类实现界面,并实现两个接口,用来开启多线程和事件监听处理。然后设计界面。需要先创建JTextField 文本框用来输入消息、JTextArea文本域用来显示聊天内容,button按钮用来发送消息等。然后将组建添加到窗体中合适的地方。添加按键监听和回车监听。

public class UDPclient extends JFrame implements Runnable,ActionListener {

JButton button = new JButton();

JLabel label_shang = new JLabel();

    JLabel label_xia = new JLabel();

    JTextArea area = new JTextArea(40, 50);

    JTextField field = new JTextField(38);

    

private String name = null;

private int Port = 9998;

private DatagramSocket DS;

private String myPort;

public UDPclient(){

this.setBounds(200, 200, 600, 400);

this.setTitle("客户端");

//对窗口进行大的布局,分为三行一列,在pBasic面板上添加上中下三个面板            JPanel pBasic=new JPanel();

        //使用BorderLayout

        pBasic.setLayout(new BorderLayout());         setContentPane(pBasic);//把面板放在窗口上

        JPanel shang=new JPanel();

        JPanel zhong=new JPanel();

        JPanel xia=new JPanel();

        //设置JPanel面板的大小

        shang.setSize(600, 100);

        zhong.setSize(600, 500);

        xia.setSize(600, 60);

        pBasic.add(shang,BorderLayout.NORTH);

        pBasic.add(zhong,BorderLayout.CENTER);

        pBasic.add(xia,BorderLayout.SOUTH);

        shang.setBackground(Color.gray);

        zhong.setBackground(Color.LIGHT_GRAY);

        xia.setBackground(Color.white);

        /*

         * 三个面板,上边放一个标签“聊天记录”,中间放一个文本域,

         * 下边分为左中右——分别放标签“输入信息“,文本框和”发送“按钮

         */

        label_shang.setText("聊天室");

        shang.add(label_shang);

        area.setLineWrap(true);// 自动换行

        JScrollPane scroll=new JScrollPane(area);// 增加滚动条,以便不增加行数

        zhong.add(scroll);

        label_xia.setText("输入信息");

        xia.add(label_xia,BorderLayout.WEST);

        xia.add(field,BorderLayout.CENTER);

        button.setText("发送");

        xia.add(button,BorderLayout.EAST);

//添加按键监听和回车监听

        button.addActionListener(this);

field.addActionListener(this);

//通过压缩自动调整各个面板

        pack();

        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);// 点关闭按钮同时退出程序

        setVisible(true);

② 使用输入对话框JOptionPane.showInputDialog,在里面输入用户名,用来区分不同的客户端,设置完以后将窗口的名称设置成输入的用户名。这里还手动输入了端口号,也可以不用。如果我输入的用户名为空,则设置成匿名用户。

name = JOptionPane.showInputDialog(null,"请输入用户名:\n","登录",JOptionPane.PLAIN_MESSAGE);

myPort = JOptionPane.showInputDialog(null,"请输入端口号:\n","端口号",JOptionPane.PLAIN_MESSAGE);

if(name == null){

this.setTitle("匿名");

name = "匿名";

}else{

this.setTitle(name);

}

③ 开始进行向服务器发送消息。这里使用了DatagramSocket的connect方法和指定服务器进行连接。连接后发送消息开启线程,并在聊天区打印出该用户的上线消息,并且打印出IP地址、端口号以及发送时间。

try{

DS = new DatagramSocket();

InetAddress address = InetAddress.getByName("Localhost");

DS.connect(address,Port);

String str = "--------( " + name + " )已上线!--------IP:"+ address + "  Port:" + myPort + "-----------";

byte[] data = str.getBytes();

DatagramPacket DP = new DatagramPacket(data,data.length);

DS.send(DP);

new Thread(this).start();//开启一个线程

} catch(Exception e) {

e.printStackTrace();

}

}

④ 重写run方法。套接字利用receive方法接收消息,然后将时间按照规定格式和其一同打印。

public void run(){//重写run()方法

try{

while(true){

byte[] data = new byte[1024];

DatagramPacket DP = new DatagramPacket(data,data.length);

DS.receive(DP);

String str = new String(DP.getData(),0,DP.getLength());

SimpleDateFormat matter = new SimpleDateFormat("MM/dd HH:mm:ss");  

Date nowTime = new Date();

       //SimpleDateFormat方式,完整输出现在时间  

area.append(  matter.format(nowTime) + "  " + str + '\n');

}

}catch(Exception ex){

ex.printStackTrace();

}

}


⑤ public void actionPerformed(ActionEvent e) 是接口ActionListener里面定义的一个抽象方法,所有实现这个接口的类都要重写这个方法。一般情况下,这是在编写GUI程序时,组件发生“有意义”的事件时会调用这个方法,比如按钮被按下,文本框内输入回车时都会触发这个事件,然后调用所编写的事件处理程序。实现过程大体为:编写一个ActionListener类的侦听器,组件注册该侦听器,侦听器内部再实现actionPerformed方法。

//监听回车和发送按钮  

button.addActionListener(this);

field.addActionListener(this);

 

public void actionPerformed(ActionEvent e){

try{

String str = name + ":" + field.getText();

byte[] dd = str.getBytes();

DatagramPacket Data = new DatagramPacket(dd,dd.length);

DS.send(Data);

field.setText("");

}catch(Exception ex){

ex.printStackTrace();

}

}

public static void main(String[] args){

new UDPclient();

}

}

关于客户端退出聊天。在代码中设置了:

setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

所以当点击窗口的关闭按钮时,会自动退出程序。

由于服务器在这里起到传送消息的作用,所以不需要实现界面,当所有客户端程序结束时,手动关闭服务器程序即可。

 

三、实验中遇到的问题及解决方案

问题:无法更改界面的字体大小。

尝试方案:添加一个函数如下——

public static void setUIFont()

{

Font f = new Font("宋体",Font.PLAIN,36);

String   names[]={ "Label", "CheckBox", "PopupMenu","MenuItem", "CheckBoxMenuItem",

"JRadioButtonMenuItem","ComboBox", "Button", "Tree", "ScrollPane",

"TabbedPane", "EditorPane", "TitledBorder", "Menu", "TextArea",

"OptionPane", "MenuBar", "ToolBar", "ToggleButton", "ToolTip",

"ProgressBar", "TableHeader", "Panel", "List", "ColorChooser",

"PasswordField","TextField", "Table", "Label", "Viewport",

"RadioButtonMenuItem","RadioButton", "DesktopPane", "InternalFrame"

};

for (String item : names) {

 UIManager.put(item+ ".font",f);

}

}

然后在主函数中首先调用这个方法,但是字体仍为默认大小。

 

四、实验最终结果展示

多人聊天界面:

 

五、实验总结

这次基于UDP协议的socket编程实验。最初我实现了客户端和服务器之间基于UDP协议的通信,然后在这个基础上加入了多线程部分以实现多人聊天,最后实现了聊天的界面。

通过递进的实验环节让我理解了UDP协议下通信的实现,并且对于多线程有了一定的了解。目前,界面部分还不够美观,以后还需要学习界面的相关知识并运用起来。

 

六、源代码(*)

UDPclient.java

import java.awt.BorderLayout;

import java.awt.Color;

import java.awt.Font;

import java.awt.event.ActionEvent;

import java.awt.event.ActionListener;

import java.net.DatagramPacket;

import java.net.DatagramSocket;

import java.net.InetAddress;

import java.text.*;

import java.util.Date;

 

import javax.swing.*;

/*客户端:创建DatagramSocket对象,创建好IP地址和端口号后,

利用DatagramSocket中的connect(ip,port)方法和服务端建立连接,然后利用DatagramSocket中的send(dp)方法发送早已准备好的数据。*/

 

public class UDPclient extends JFrame implements Runnable,ActionListener {

//JTextField field = new JTextField("");

//JTextArea area = new JTextArea("【欢迎进入聊天室】\n");

JButton button = new JButton();

JLabel label_shang = new JLabel();

    JLabel label_xia = new JLabel();

    JTextArea area = new JTextArea(40, 50);

    JTextField field = new JTextField(38);

    

private String name = null;

private int Port = 9998;

private DatagramSocket DS;

private String myPort;

public UDPclient(){

this.setBounds(200, 200, 600, 400);

this.setTitle("客户端");

//对窗口进行大的布局,分为三行一列,在pBasic面板上添加三个面板shang zhong xia

        JPanel pBasic=new JPanel();

        //使用BorderLayout

        pBasic.setLayout(new BorderLayout());//不设置默认也是这种布局模式

        setContentPane(pBasic);//把面板放在窗口上

        JPanel shang=new JPanel();

        JPanel zhong=new JPanel();

        JPanel xia=new JPanel();

        //设置JPanel面板的大小

        shang.setSize(600, 100);

        zhong.setSize(600, 500);

        xia.setSize(600, 60);

        pBasic.add(shang,BorderLayout.NORTH);

        pBasic.add(zhong,BorderLayout.CENTER);

        pBasic.add(xia,BorderLayout.SOUTH);

        shang.setBackground(Color.gray);

        zhong.setBackground(Color.LIGHT_GRAY);

        xia.setBackground(Color.white);

        /*

         * 三个面板,上边放一个标签“聊天记录”,中间放一个文本域,

         * 下边分为左中右——分别放标签“输入信息“,文本框和”发送“按钮

         */

        label_shang.setText("聊天室");

        shang.add(label_shang);

        area.setLineWrap(true);// 自动换行

        JScrollPane scroll=new JScrollPane(area);// 增加滚动条,以便不增加行数

        zhong.add(scroll);

        label_xia.setText("输入信息");

        xia.add(label_xia,BorderLayout.WEST);

        xia.add(field,BorderLayout.CENTER);

        button.setText("发送");

        xia.add(button,BorderLayout.EAST);

        button.addActionListener(this);

field.addActionListener(this);

//监听回车

//通过压缩自动调整各个面板

        pack();

        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);// 点关闭按钮同时退出程序

        setVisible(true);

name = JOptionPane.showInputDialog(null,"请输入用户名:\n","登录",JOptionPane.PLAIN_MESSAGE);

myPort = JOptionPane.showInputDialog(null,"请输入端口号:\n","端口号",JOptionPane.PLAIN_MESSAGE);

if(name == null){

this.setTitle("匿名");

name = "匿名";

}else{

this.setTitle(name);

}

try{

DS = new DatagramSocket();

InetAddress address = InetAddress.getByName("Localhost");

DS.connect(address,Port);

String str = "--------( " + name + " )已上线!--------IP:"+ address + "  Port:" + myPort + "-----------";

byte[] data = str.getBytes();

DatagramPacket DP = new DatagramPacket(data,data.length);

DS.send(DP);

new Thread(this).start();//开启一个线程

} catch(Exception e) {

e.printStackTrace();

}

}

public void run(){//重写run()方法

try{

while(true){

byte[] data = new byte[1024];

DatagramPacket DP = new DatagramPacket(data,data.length);

DS.receive(DP);

String str = new String(DP.getData(),0,DP.getLength());

SimpleDateFormat matter = new SimpleDateFormat("MM/dd HH:mm:ss");  

Date nowTime = new Date();

       //SimpleDateFormat方式,完整输出现在时间  

area.append(  matter.format(nowTime) + "  " + str + '\n');

}

}catch(Exception ex){

ex.printStackTrace();

}

}

public void actionPerformed(ActionEvent e){

try{

String str = name + ":" + field.getText();

byte[] dd = str.getBytes();

DatagramPacket Data = new DatagramPacket(dd,dd.length);

DS.send(Data);

field.setText("");

}catch(Exception ex){

ex.printStackTrace();

}

}

public static void main(String[] args){

new UDPclient();

}

}

UDPserver.java

import java.awt.Font;

import java.net.DatagramPacket;

import java.net.DatagramSocket;

import java.net.SocketAddress;

import java.util.ArrayList;

 

import javax.swing.UIManager;

 

public class UDPserver implements Runnable{

private DatagramSocket DS;

private int Port = 9998;

private ArrayList<SocketAddress> clients = new ArrayList<SocketAddress>(); //保存客户端IP地址

public UDPserver()throws Exception{

try {

DS = new DatagramSocket(Port);//创建服务器端DatagramSocket,指定端口9998

new Thread(this).start();//开启服务器端线程

} catch (Exception ex) {

ex.printStackTrace();

}

}

public void run(){//重写run方法

try{

while(true){

byte[] data = new byte[1024];//创建字节数组,指定可以接收的数据报的大小

DatagramPacket DP = new DatagramPacket(data,data.length);

DS.receive(DP);//接收报文,此方法在接收到数据报之前会一直阻塞

SocketAddress clientip = DP.getSocketAddress(); //得到客户端的IP+PORT

if(!clients.contains(clientip)){//把客户端的IP+PORT存起来

clients.add(clientip);

}

this.sendAllClients(DP);//服务器向每一个客户端发送消息,以实现多人聊天。

}

}catch(Exception ex){

ex.printStackTrace();

}

}

//服务器向每一个客户端发送消息,以实现多人聊天。

private void sendAllClients(DatagramPacket dp) throws Exception {

for(SocketAddress addr : clients){

DatagramPacket dd = new DatagramPacket(dp.getData(),dp.getLength(),addr);

DS.send(dd);

}

}

//main函数中直接new一个服务器的实例,调用其构造方法。

public static void main(String[] args) throws Exception{

new UDPserver();

}

}

  • 12
    点赞
  • 77
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值