Java第十四课——相声和群口相声

Java第十四课——相声和群口相声

相声,我们知道有一个逗哏一个捧哏,也就是一唱一和,也就相当于双向的交流。群口相声同理,三人及三人以上,也就是群聊的效果。那么这次就来实现一个服务器和客户端双向的交流,也是客户和客户之间的交流。
其实二者实现过程同理,就拿vx来举个例子,我输入了一条消息,先发送给服务器,而这条消息是需要被我自己和接收消息的人看到的,所以服务器只是获取到我自己和接收消息的人的输出流,输出这句话就完成了。群聊同理,获取当前群聊所有人的输出流,全部输出就可以了。
先把单向通信的代码放上
服务器端:

public class Server {
	ServerSocket serversocket;
	public void create(){
		try{
			serversocket = new ServerSocket(9876);
			System.out.println("ServerSocket: loading successfully");
		} catch (IOException e){
			e.printStackTrace();
		}
	}
	
	public void conn(){
		try {
			//等待用户连接
			Socket socket = serversocket.accept();
			System.out.println("ServerSocket: user enter");	
			
			//获取输入输出流
			InputStream in = socket.getInputStream();
			OutputStream out = socket.getOutputStream();
			
			int onebyte = in.read();
			char ch = (char)onebyte;
			System.out.println("ServerSocket: has read: " + ch);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public static void main(String[] args){
		Server server = new Server();
		server.create();
		server.conn();
	}
}

客户端:

public class UserSocket {
	String ip = "0.0.0.0";//IP地址
	int port = 9876;
	OutputStream out;//客户端的输出流
	InputStream in;//客户端输入流
	
	public void userUI(){
		JFrame frame = new JFrame();
		frame.setSize(300,300);
		frame.setLayout(new FlowLayout());
		frame.setDefaultCloseOperation(3);
		
		JTextField field = new JTextField(20);//输入框
		JButton btn = new JButton("发送"); 
		frame.add(field);
		frame.add(btn);
		frame.setVisible(true);
		
		ActionListener l = new ActionListener() {		
			@Override
			public void actionPerformed(ActionEvent e) {
				// TODO Auto-generated method stub
				try {
					System.out.println("User: field:"+field.getText());
					out.write(field.getText().getBytes());
				} catch (IOException e1) {
					// TODO Auto-generated catch block
					e1.printStackTrace();
				}
			}
		};
		
		btn.addActionListener(l);
	}
	
	public void conn(){
		try {
			Socket socket = new Socket(ip, port);
			System.out.println("User: connected");			
			
			in = socket.getInputStream();
			out = socket.getOutputStream();
		} catch (UnknownHostException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace(); 
		}
	}

	public static void main(String[] args) {
		UserSocket us = new UserSocket();
		us.conn();	
	}
}

动手之前先梳理一下问题,明确一下目标:
1、目前只能读取一个字节,而中国汉字都是一个字符,如何实现发送文字?
2、多人聊天的话,会有多个客户端连接服务器,怎么处理?会遇到什么问题?
3、需要有聊天框来显示聊天内容,且JTextField不能满足发送多行文本的需要
1和3比较容易解决,主要是2。下面先解决1和3.

一、聊天界面优化

修改一下客户端的界面就好
1、首先把界面加大,放下更多组件
2、其次用JTextArea来代替JTextField,且用JTextArea显示聊天内容
3、加入一些个性化设计
(直接看长代码很累,但一小段一小段的代码也看得很累,所以我把改动的地方在注释里改的稍微明显了一点,就不用一行一行的看了)

public void userUI(){
	JFrame frame = new JFrame();
	frame.setTitle("老王加密聊天v1.0");//————————————————————————————————————增加Title
	frame.setSize(600,300);//————————————————————————————————————————————————增大界面
	frame.setLayout(new FlowLayout());
	frame.setDefaultCloseOperation(3);
		
	//—————————————————————————————————————inputArea和outputArea都是全局变量JTextArea
	inputArea = new JTextArea(10,15);//——————————————————————————————————————客户输入
	outputArea = new JTextArea(10,15);//———————————————————————————————————服务器输出
	JButton btn = new JButton("发送"); 

	outputArea.setEditable(false);
	outputArea.setBackground(new Color(0xdcdcdc));//———————聊天显示框设置为灰色不可编辑
	
	frame.add(inputArea);
	frame.add(btn);
	frame.add(outputArea);//——————————————————————————————————————把三个组件加到窗体上
	frame.setVisible(true);
		
	ActionListener l = new ActionListener() {		
		@Override
		public void actionPerformed(ActionEvent e) {
			// TODO Auto-generated method stub
			try {
				//——————————————————————————————————————————————把field改成inputArea
				System.out.println("User: inputArea:"+inputArea.getText());
				out.write(inputArea.getText().getBytes());
			} catch (IOException e1) {
				// TODO Auto-generated catch block
				e1.printStackTrace();
			}
		}
	};
	
	btn.addActionListener(l);
}

到这里还没有结束,毕竟没有测试(这很重要),测试方法就是连接服务器,敲一个字节看看有没有输出。

二、增加发送内容的样式

我们知道,InputStream和OutputStream只能发送和接收byte,上节课有用到更加好用的DataInputStream和DataOutputStream,不会用的小伙伴去看上节课内容:
迷你会议的实现中使用了DataInputStream和DataOutputStream

Java第十三课——客户端与服务器单向多样化数据传输

但毕竟我们发送的消息不可能只是Int,查看DataInputStream和DataOutputStream的方法可以看到有这两个方法:
writeUTF
readUTF
既然白给了就用它!
1、先把客户端的OutputStream和InputStream改成DataOutputStream和DataInputStream

public void conn(){
	try {
		Socket socket = new Socket(ip, port);
		System.out.println("User: connected");			
		
		//—————————————————————因InputStream和OutputStream后面不再使用,可以变成局部变量了
		InputStream in = socket.getInputStream();
		OutputStream out = socket.getOutputStream();
			
		//——————————————————————————但DataInputStream和DataOutputStream需要变成全局变量
		dout = new DataOutputStream(out);
		din = new DataInputStream(in);
	} catch (UnknownHostException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	} catch (IOException e) {
		// TODO Auto-generated catch block
		e.printStackTrace(); 
	}
}

2、修改ActionListener里的write方法
(ActionListener在userUI的方法里作为内部类)

ActionListener l = new ActionListener() {		
	@Override
	public void actionPerformed(ActionEvent e) {
		// TODO Auto-generated method stub
		try {
			System.out.println("User: inputArea:"+inputArea.getText());
			//————————————————————————改成用dout(DataOutputStream)来发,且getBytes要删去
			dout.writeUTF(inputArea.getText());
		} catch (IOException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}
	}
};	

3、把服务器端的InputStream变成DataInputStream,并修改read方法

public void conn(){
	try {
		//等待用户连接
		Socket socket = serversocket.accept();
		System.out.println("ServerSocket: user enter");	
			
		//获取输入输出流
		InputStream in = socket.getInputStream();
		OutputStream out = socket.getOutputStream();
			
		//——————————————————————————————————————————————DataInputStream din作为全局变量
		din = new DataInputStream(in);
		//——————————————————————————————————————————————————————用readUTF读取消息并显示
		String sentence = din.readUTF();
		System.out.println("ServerSocket: has read: " + sentence);
	} catch (IOException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	}
}

进行测试看到如下结果:
readUTF/writeUTF测试
在客户端和服务器都加上while(true)就可以持续的发送和读取消息了

三、群聊雏形

回到上面的第二个问题:2、多人聊天的话,会有多个客户端连接服务器,怎么处理?会遇到什么问题?
先来看看会遇到什么问题?
当客户端连接上服务器后,服务器端会执行System.out.println(“ServerSocket: user enter”); ,那么如果已经有一个客户端连接上了服务器,当第二个服务器去连接时,服务器端的输出如下
多个客户端连接
可以看到,服务器只输出了一次ServerSocket: user enter
并且在第二个客户端输入后发送,服务器没有收到。
我们查看服务器的代码:

1|	public void conn(){
2|		try {
3|			//等待用户连接
4|			Socket socket = serversocket.accept();
5|			System.out.println("ServerSocket: user enter");	
6|			
7|			//获取输入输出流
8|			InputStream in = socket.getInputStream();
9|			OutputStream out = socket.getOutputStream();
10|
11|			din = new DataInputStream(in);
12|			
13|			//读取消息
14|			String sentence = din.readUTF();
15|			System.out.println("ServerSocket: has read: " + sentence);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

不难看出,当第一个客户端进入后,服务器先获取输入输出流,然后就会卡在第14行,等待客户端发来消息,当第二个客户端连接时,如果第一个客户端没有输入,服务器仍在14行,就会导致无响应,而如果第一个客户输入了,因为没写while(true),服务器收完消息就会关闭了。
那么如何处理呢?
把这个问题想象成一个模型,假设现在有个用户打给警察局报警,此时如果又有一个用户来报警,难道会打不通吗?警察局的处理方式就是叫一个接线员来接电话,这样就不会使第二个人打进了的时候占线了。而我们日常打电话时,显示正在通话中,就是因为我们只有一个人,我们没有接线员,所以导致占线打不通。
所以处理办法就是,叫”一个人“来帮助我们处理这个用户,那便是多线程
服务器只管连接,有人连接了,就分配一个线程处理
1、先修改服务器
服务器要做的就是等待连接,启动线程(这个线程假设名字为ChatTh)

public class Server {
	ServerSocket serversocket;
	public void create(){
		try{
			serversocket = new ServerSocket(9876);
			System.out.println("ServerSocket: loading successfully");
		} catch (IOException e){
			e.printStackTrace();
		}
	}

	public void conn(){
		try {
			//等待用户连接
			Socket socket = serversocket.accept();
			System.out.println("ServerSocket: user enter");	
			
			//———————————————————————————————————————————————————————这里往下该写什么?
			
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public static void main(String[] args){
		Server server = new Server();
		server.create();
		server.conn();
	}	

上面的代码并不完整,主要是conn()方法里应该写什么?
是获取输入输出流,再传给线程?还是直接启动线程,把socket传过去?
2、ChatTh线程
通过构造方法获取Socket对象,然后获取输入输出流,转换成DataInputStream和DataOutputStream。然后获取消息输出就可以了

public class ChatTh implements Runnable{
	Socket socket;//用户
	
	DataInputStream din;
	DataOutputStream dout;
	
	public ChatTh(Socket socket) {
		super();
		this.socket = socket;
		//初始化时获取输入输出流
		getStream();
	}

	//获取输入输出流
	public void getStream(){		
		try{
			InputStream in = socket.getInputStream();
			OutputStream out = socket.getOutputStream();
			
			din = new DataInputStream(in);
			dout = new DataOutputStream(out);
		}catch(IOException e){
			e.printStackTrace();
		}		
	}
	
	@Override
	public void run() {
		try{
			while(true){//持续读取消息
				String sentence = din.readUTF();
				System.out.println("ServerSocket: has read: " + sentence);
			}			
		}catch(IOException e){
			e.printStackTrace();
		}
	}
}

再完善一下服务器conn方法:

public void conn(){
	try {
		while(true){//持续等待用户连接
			//等待用户连接
			Socket socket = serversocket.accept();
			System.out.println("ServerSocket: user enter");	
	
			//启动线程进行管理
			ChatTh chatth = new ChatTh(socket);
			new Thread(chatth).start();
		}
	} catch (IOException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	}
}

现在测试一下看看多个客户端连接有没有输出!
聊天线程,输出版本
3、服务器端获取所有用户的输出流
现在距离群聊雏形还差一步,在文章最开始时也说了,获取群聊内所有用户的输出流,把服务器收到的聊天内容进行输出,就是群聊的效果。
这里用列表来收集用户的输出流,而列表的创建要在服务器的主方法里,不能在线程里。然后传给ChatTh,当一个用户发送消息之后,遍历列表发送

public class Server {
	ServerSocket serversocket;
	//————————————————————————————————————————————————————————————————————————创建列表
	ArrayList<DataOutputStream> doutlist = new ArrayList<DataOutputStream>();
	public void create(){...}
	public void conn(){
		try {
			while(true){//持续等待用户连接
				//等待用户连接
				Socket socket = serversocket.accept();
				System.out.println("ServerSocket: user enter");	
	
				//启动线程进行管理
				//————————————————————————————————————把列表传过去,在ChatTh用构造方法接收
				ChatTh chatth = new ChatTh(socket, doutlist);
				new Thread(chatth).start();
			}
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	public static void main(String[] args){...}
}

ChatTh这边修改构造方法。在getStream方法中,获取到dout之后向列表里添加。最后在获取消息时,遍历列表进行输出

public class ChatTh implements Runnable{
	Socket socket;//用户
	
	DataInputStream din;
	DataOutputStream dout;
	
	ArrayList<DataOutputStream> doutlist;//——————————————————————————所有用户的输出流
	//———————————————————————————————————————————————————————————————————修改构造方法
	public ChatTh(Socket socket, ArrayList<DataOutputStream> doutlist) {
		super();
		this.socket = socket;
		this.doutlist = doutlist;
		getStream();
	}

	//获取输入输出流
	public void getStream(){		
		try{
			InputStream in = socket.getInputStream();
			OutputStream out = socket.getOutputStream();
			
			din = new DataInputStream(in);
			dout = new DataOutputStream(out);
			//————————————————————————————————————————————————————————————加入到列表
			doutlist.add(dout);
		}catch(IOException e){
			e.printStackTrace();
		}		
	}
	
	@Override
	public void run() {
		try{
			while(true){
				String sentence = din.readUTF();
				System.out.println("ServerSocket: has read: " + sentence);
				//————————————————————————————————————————————————————遍历全部进行输出
				for(int i = 0; i < doutlist.size(); i++){
					DataOutputStream douts = doutlist.get(i);
					douts.writeUTF(sentence);
				}				
			}		
		}catch(IOException e){
			e.printStackTrace();
		}
	}
}

4、客户端把收到的消息放到显示框上
客户端这边,连接上服务器之后,要做的事情就是:点击按钮发送消息,这个交给ActionListener去做,接收消息就可以通过while(true)来实现。
在客户端的conn()方法中加入while(true):
这里因为在conn()方法里写了while(true),所以打开窗体的方法不能在conn()方法之后调用,但如果没有连接上服务器之前打开窗体又不太合适,所以把userUI()方法放进conn()方法中

public void conn(){
	try {
		Socket socket = new Socket(ip, port);
		System.out.println("User: connected");			
			
		in = socket.getInputStream();
		out = socket.getOutputStream();
			
		dout = new DataOutputStream(out);
		din = new DataInputStream(in);
		
		//——————————————————————————————————————————————————————————————————打开窗体
		userUI();

		//——————————————————————————————————————————————————————————————持续读取消息
		while(true){
			//读取到的消息
			String sentence = din.readUTF();
			System.out.println("User: has recieved: " + sentence);
			//显示在聊天窗体上
			outputArea.append(sentence + "\r\n");
		}
	} catch (UnknownHostException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	} catch (IOException e) {
		// TODO Auto-generated catch block
		e.printStackTrace(); 
	}	
}

最后来测试一下:
群聊实现
那么到这里,本节课内容就实现了。当然可以进一步迭代优化:
1、仿视频软件,又能发视频又能发文字,考虑使用并发+报文头或多端口来实现。
2、客户端退出问题,如果连接上服务器之后直接退出了,就会报错,并且在doutlist的列表里就会出现僵尸dout,考虑使用心跳机制

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值