当前位置:   article > 正文

计算机网络实验:套接字及客户服务器应用程序基础(Java实现点对点聊天的实用程序)_java局域网内两点之间通讯csdn

java局域网内两点之间通讯csdn

实验名称:

套接字及客户服务器应用程序基础

实验介绍:

本次实验要求自己动手实现一个能够在局域网中进行点对点聊天的实用程序。

实验目的:

  1. 熟悉Visual C++的基本操作,亦可采用Java等其它面向对象的编程语言。
  2. 基本了解基于对话框的windows应用程序的编写过程。
  3. 实现UDP或TCP套接字编程(1人1组)。

背景知识和准备:

参考实验《套接字及客户/服务器应用程序基础》,Winsock编程基础。

  1. 网络各层常见协议
    应用层:TFTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet 等等
    传输层:TCP,UDP
    网络层:IP,ICMP,OSPF,EIGRP,IGMP
    数据链路层:SLIP,CSLIP,PPP,MTU
    每一抽象层建立在低一层提供的服务上,并且为高一层提供服务。
  2. Socket通信模型
    在这里插入图片描述
  3. Socket通信过程
    服务器端:
     打开一个通信通道并告知本地主机,它愿意在某一公认地址上的某端口接收客户请求;
     等待客户请求到达该端口;
     接收到客户端的服务请求时,处理该请求并发送应答信号。接收到并发服务请求,要激活一个新进程来处理这个客户请求。新进程处理此客户请求,并不需要对其它请求作出应答。服务完成后,关闭此新进程与客户的通信链路,并终止。
     返回第(2)步,等待另一客户请求;
     关闭服务器。
    客户端:
     打开一个通信通道,并连接到服务器所在主机的特定端口;
     向服务器发服务请求报文,等待并接收应答;继续发送请求;
     请求结束后关闭通信通道并终止。
    综上,客户与服务器进程的作用是非对称的,因此代码不同,需要为客户与服务器写两个类。服务器进程一般是先启动的。只要系统运行,该服务进程一直存在,直到正常或强迫终止。
  4. 一些比较重要的API
    a) 创建套接字──socket()
    应用程序在使用套接字前,首先必须拥有一个套接字,系统调用socket()向应用程序提供创建套接字的手段,其调用格式如下:
    SOCKET PASCAL FAR socket(int af, int type, int protocol)
    参数protocol说明该套接字使用的特定协议,如果调用者不希望特别指定使用的协议,则置为0,使用默认的连接模式。根据这三个参数建立一个套接字,并将相应的资源分配给它,同时返回一个整型套接字号。
    b) 指定本地地址──bind()
    当一个套接字用socket()创建后,存在一个名字空间(地址族),但它没有被命名。bind()将套接字地址(包括本地主机地址和本地端口地址)与所创建的套接字号联系起来,即将名字赋予套接字,以指定本地半相关。其调用格式如下:
    int PASCAL FAR bind(SOCKET s, const struct sockaddr FAR * name, int namelen)
    参数s是由socket()调用返回的并且未作连接的套接字描述符(套接字号)。参数name 是赋给套接字s的本地地址(名字),其长度可变,结构随通信域的不同而不同。namelen表明了name的长度。如果没有错误发生,bind()返回0。否则返回SOCKET_ERROR。
    c) 建立套接字连接——connect()与accept()
    这两个系统调用用于完成一个完整相关的建立,其中connect()用于建立连接。accept()用于使服务器等待来自某客户进程的实际连接。
    connect()的调用格式如下:
    int PASCAL FAR connect(SOCKET s, const struct sockaddr FAR * name, int namelen)
    参数s是欲建立连接的本地套接字描述符。参数name指出说明对方套接字地址结构的指针。对方套接字地址长度由namelen说明。
    accept()的调用格式如下:
    SOCKET PASCAL FAR accept(SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen)
    参数s为本地套接字描述符,在用做accept()调用的参数前应该先调用过listen()。addr 指向客户方套接字地址结构的指针,用来接收连接实体的地址。addr的确切格式由套接字创建时建立的地址族决定。addrlen 为客户方套接字地址的长度(字节数)。如果没有错误发生,accept()返回一个SOCKET类型的值,表示接收到的套接字的描述符。否则返回值INVALID_SOCKET。
    d) 监听连接——listen()
    此调用用于面向连接服务器,表明它愿意接收连接。listen()需在accept()之前调用,其调用格式如下:
    int PASCAL FAR listen(SOCKET s, int backlog)
    参数s标识一个本地已建立、尚未连接的套接字号,服务器愿意从它上面接收请求。backlog表示请求连接队列的最大长度,用于限制排队请求的个数,目前允许的最大值为5。如果没有错误发生,listen()返回0。否则它返回SOCKET_ERROR。
    e) 数据传输——send()与recv()
    当一个连接建立以后,就可以传输数据了。常用的系统调用有send()和recv()。send()调用用于s指定的已连接的数据报或流套接字上发送输出数据,格式如下:
    int PASCAL FAR send(SOCKET s, const char FAR *buf, int len, int flags)
    参数s为已连接的本地套接字描述符。buf 指向存有发送数据的缓冲区的指针,其长度由len 指定。flags 指定传输控制方式,如是否发送带外数据等。如果没有错误发生,send()返回总共发送的字节数。否则它返回SOCKET_ERROR。
    recv()调用用于s指定的已连接的数据报或流套接字上接收输入数据,格式如下:
    int PASCAL FAR recv(SOCKET s, char FAR *buf, int len, int flags)
    f) 关闭套接字——closesocket()
    closesocket()关闭套接字s,并释放分配给该套接字的资源;如果s涉及一个打开的TCP连接,则该连接被释放。closesocket()的调用格式如下:
    BOOL PASCAL FAR closesocket(SOCKET s)
    参数s待关闭的套接字描述符。如果没有错误发生,closesocket()返回0。否则返回值SOCKET_ERROR。

实验过程:

  1. 选择Java语言与Eclipse开发工具。
    本实验采用TCP协议,基于Java GUI,通过socket编程实现点对点聊天室。
  2. 详细设计
    2.1 服务器端
     类设计

public class SocketServer extends JFrame

该类继承自JFrame,为Socket服务器的主类。
 类的成员变量

private JPanel contentPane; 
private JTextArea centerTextArea; 	
private List<Client> onlineList =new ArrayList<Client>(); 	
SimpleDateFormat dateFormat= new SimpleDateFormat("yyyy/MM/dd hh:mm:ss");
Date date;
  • 1
  • 2
  • 3
  • 4
  • 5

其中contentPane、centerTextArea为GUI组件。该页面仅包含一个TextArea,用于显示聊天信息。值得一提的是date、dateFormat用于输出每条消息的时间,以“年/月/日 时:分:秒”的格式给出。
 主函数

public static void main(String[] args) {
	try {
		SocketServer frame = new SocketServer();
		ServerSocket serverSocket = new ServerSocket(5566);
		frame.setVisible(true);
		while(true) {
			Socket socket = serverSocket.accept();
			frame.getClient(socket);
		}
	} catch (Exception e) {
		e.printStackTrace();
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

主函数的设计思路是,创建frame对象,在其函数内设置其具体布局;创建一个ServerSocket,并绑定端口号为5566。注意若该端口号被占用,则服务器不能使用,更改端口号目前仅能通过修改代码实现。之后,在一个while(true)循环内,利用Socket服务器的accept()方法获取客户端Socket对象,每获取到一个Socket对象,调用getClient()函数处理。
 构造器
构造器主要用于实现布局,代码如下:

public SocketServer() {
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		setBounds(100, 100, 400, 400);
		setResizable(false);//窗口大小不可调整
		setTitle("Server");
		setLocationRelativeTo(null);//窗口居中
		
		contentPane = new JPanel();
		contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
		contentPane.setLayout(new BorderLayout(0, 0));
		setContentPane(contentPane);
		
		centerTextArea=new JTextArea();
		centerTextArea.setEditable(false);
		centerTextArea.setBackground(new Color(247, 247, 247));
		add(new JScrollPane(centerTextArea),BorderLayout.CENTER);

		Date date = new Date();
		SimpleDateFormat dateFormat= new SimpleDateFormat("yyyy/MM/dd hh:mm:ss");
		centerTextArea.append(dateFormat.format(date)+"\n");
		centerTextArea.append("服务器已运行\n\n");
		centerTextArea.setCaretPosition(centerTextArea.getText().length());				
	}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

注意到一些细节,比如设置窗口大小不可以调整、设置窗口居中、设置文本区域不可编辑、为体现出不可编辑使用浅灰色表示。值得一提的是,在这里我使用了setCaretPosition()方法控制滚动条,使每次文本区域更新后,滚动条都能在JTextArea最底端。
 客户端Socket对象的处理
由getClient()函数实现,代码为:

  private void getClient(Socket socket) { 
      try {
    	  BufferedReader in = new BufferedReader(new InputStreamReader(
    			  socket.getInputStream()));
    	  String firstInfo=in.readLine();
    	  date = new Date();
    	  centerTextArea.append(dateFormat.format(date)+"\n");
    	  centerTextArea.append(firstInfo+"\n\n");
    	  centerTextArea.setCaretPosition(centerTextArea.getText().length());	
    	      Client client = new Client(firstInfo,socket); 
	      onlineList.add(client);
	      Thread thread =new Thread(client); //创建客户端处理线程  
	      thread.start();//启动线程	
      } catch (IOException e) {
    	  e.printStackTrace();
      }		     	
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

首先初始化输入流并从网络缓冲区读取客户端传输的数据,接着在服务器端初始化一个Client对象,并将其添加到客户端对象队列中用于记录当前在线用户,然后为每个客户端创建一个线程,并开启该线程。
 内部类Client
该类用于处理客户端对象,实现了Runnable接口。服务器相当于提供了点到点的中转功能,具体通过遍历当前在线列表中的Client对象实现。

  class Client implements Runnable  {  
	  String name; //客户端名字
	  Socket socket = null;//客户端Socket对象
	  BufferedReader in;
	  PrintWriter out;
	  Client(String name,Socket socket) {
		  this.socket = socket; 
		  this.name=name;
		  try {
			  in = new BufferedReader(new InputStreamReader(
					  socket.getInputStream()));
			  out = new PrintWriter(socket.getOutputStream());
			
		  } catch (IOException e) {
			  e.printStackTrace();
		  }  
	  }
	  
    public void run(){  
  		try {
  			while (true) {
  				String str = in.readLine();
  				if(str.split(":")[1].charAt(0)!='(') {
  					//点对点中转
	  				for(Client client: onlineList) {
	  					if(client!=this) {
	  						client.send(str);
	  					}					
	  				}
  				} else {
  					String temp1=str.split("\\(")[1];
  					String temp2=temp1.split("\\)")[0];
  					//点对点中转
	  				for(Client client: onlineList) {
	  					if(client!=this && client.name.equals(temp2)) {
	  						client.send(str);
	  					}					
	  				}
  					
  				}
  				date = new Date();
				centerTextArea.append(dateFormat.format(date)+"\n");
  				centerTextArea.append(str+"\n\n");
  				centerTextArea.setCaretPosition(centerTextArea.getText().length());				
  				
  				if (str.equals("keyToEndTheRoom"))
  					break;
  			}
  			
			try {
				socket.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
			
  		} catch (IOException e1) {
  			e1.printStackTrace();
  		}
  	} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59

2.2 客户端
 类设计

public class SocketClient extends JFrame

该类继承自JFrame,为Socket客户端的主类。
 类的成员变量

private JPanel contentPane;
private JLabel state;
private JTextArea textArea,inputArea;
JScrollPane textJScrollPane,inputJScrollPane;
private JButton send,clear;
private String name,ip;
private Socket server;
private BufferedReader in;
private PrintWriter out;
private SimpleDateFormat dateFormat= new SimpleDateFormat("yyyy/MM/dd hh:mm:ss");
private Date date;
private Thread client=new Thread(new Listener());
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

其中contentPane、state、textArea、inputArea等为GUI组件。该页面包含一个textArea用于显示聊天信息,并有一个inputArea用于输入信息。用于发送消息的按钮与用于值得一提的是date、dateFormat用于输出每条消息的时间,以“年/月/日 时:分:秒”的格式给出。

 主函数

public static void main(String[] args) {
	try {
		SocketClient frame = new SocketClient();
		frame.setVisible(true);
		frame.start();
		
	} catch (Exception e) {
		e.printStackTrace();
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

主函数的设计思路是,创建frame对象,之后直接调用start()函数创建Socket对象并开启客户端线程,与服务器进行连接。
 构造器

public SocketClient() {
	setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	setBounds(100, 100, 400, 400);
	setResizable(false);//窗口大小不可调整
	setTitle("Client");
	setLocationRelativeTo(null);//窗口居中
	contentPane = new JPanel();
	contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
	setContentPane(contentPane);
	contentPane.setLayout(null);
	
	clear = new JButton("清屏");
	clear.setBounds(56, 316, 123, 29);
	contentPane.add(clear);
	
	send = new JButton("发送");
	send.addActionListener(new ActionListener() {
		public void actionPerformed(ActionEvent arg0) {
		}
	});
	send.setBounds(217, 316, 123, 29);
	contentPane.add(send);
	
	inputArea = new JTextArea();
	inputArea.setBounds(15, 231, 364, 70);
	//contentPane.add(inputArea);
	
	inputJScrollPane=new JScrollPane(inputArea);
	inputJScrollPane.setBounds(15, 231, 364, 70);
	contentPane.add(inputJScrollPane);
	
	textArea = new JTextArea();
	textArea.setEditable(false);
	textArea.setBounds(15, 37, 364, 179);
	textArea.setBackground(new Color(247, 247, 247));
	//contentPane.add(textArea);
	textJScrollPane=new JScrollPane(textArea);
	textJScrollPane.setBounds(15, 37, 364, 179);
	textJScrollPane.setBackground(new Color(247, 247, 247));
	contentPane.add(textJScrollPane);
	
	name=JOptionPane.showInputDialog(this,"请输入您的昵称",JOptionPane.QUESTION_MESSAGE);
	ip=JOptionPane.showInputDialog(this,"请输入服务器IP地址",JOptionPane.QUESTION_MESSAGE);
	
	state=new JLabel("欢迎您,"+name);
	state.setBounds(5, 5, 384, 21);
	state.setHorizontalAlignment(JLabel.LEFT);//居右
	getContentPane().add(state);
	send.addActionListener(new ActionListener() {			
		@Override
		public void actionPerformed(ActionEvent e) {
			String str=name+":"+inputArea.getText();//封装
			date = new Date();
			textArea.append(dateFormat.format(date)+"\n");
			textArea.append(str+"\n\n");
			textArea.setCaretPosition(textArea.getText().length());			
			inputArea.setText("");			
			try {					
				out.println(str);
				out.flush();
				if (str.split(":")[1].equals("keyToEndTheRoom")) {
					server.close();
				}					
			} catch (Exception e2) {
				JOptionPane.showMessageDialog(SocketClient.this, "发送失败");
				e2.printStackTrace();
			}				
		}
	});
	
	clear.addActionListener(new ActionListener() {				
		@Override
		public void actionPerformed(ActionEvent e) {
			textArea.setText("");
		}
	});	
	
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78

构造器主要用于布局,同时提示用户输入服务器的IP地址与聊天昵称。同时send与clear两个按钮需要绑定动作,前者对应发送消息并清空输入框,后者对应清空聊天显示区域。
 创建Socket
start()函数用于创建Socket对象,代码如下:

private void start() throws UnknownHostException, IOException {
	server = new Socket(ip, 5566);
	in=new BufferedReader(new InputStreamReader(server.getInputStream()));
	out=new PrintWriter(server.getOutputStream());
	client.start();
	date = new Date();
	textArea.append(dateFormat.format(date)+"\n");
	textArea.append(name+"已进入聊天室\n\n");
	textArea.setCaretPosition(textArea.getText().length());			
	try {					
		out.println(name+"已进入聊天室");
		out.flush();				
	} catch (Exception e2) {
		JOptionPane.showMessageDialog(SocketClient.this, "发送失败");
		e2.printStackTrace();
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

客户端创建一个Socket对象绑定ip地址和服务器监听的端口号,这里即5566端口,之后初始化网络输入输出流。Socket对象创建成功之后,在聊天区域内显示该用户已进入聊天室,并将这条消息发送给服务器端。
 消息监听
客户端需要监听来自服务器的socket消息:

class Listener implements Runnable{
	@Override
	public void run() {
		try {
			while(server!=null)
			{
				String str =in.readLine();
				date = new Date();
				textArea.append(dateFormat.format(date)+"\n");
				textArea.append(str+"\n\n");
				textArea.setCaretPosition(textArea.getText().length());
			}
		} catch (Exception e) {}
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

这里实现了一个Runnable接口,每接收到一条消息,将其打印到聊天区域。 需要注意的细节是时间的处理。

  1. 运行演示
    首先运行服务器端:

在这里插入图片描述
提示服务器已运行与当前时间。之后运行客户端,我们首先运行一个客户端。首先输入昵称。

在这里插入图片描述
之后输入服务器IP地址。

在这里插入图片描述
登陆成功,当前在线的所有用户(包括自己)将收到“qiao已进入聊天室”消息。
在这里插入图片描述
尝试发送消息:
在这里插入图片描述
若消息过多,则会自动出现滚轮,且滚轮会自动定位在文字最底端。
在这里插入图片描述
之后,如果其他用户登录,当前在线的所有用户将收到登陆提示:
在这里插入图片描述
若用户选择私聊,则输入消息为(发送对象名)消息,如下所示:

在这里插入图片描述
观察到A发送给B一条消息“hi”,仅B能收到、服务器端显示,当前在线的C不能收到。


完整代码下载地址:
https://download.csdn.net/download/qq_41112170/76748059

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/172071
推荐阅读
相关标签
  

闽ICP备14008679号