admin 管理员组

文章数量: 887019

java入门(5)——通信初步(仿腾讯会议)

  • 一、TCP/IP初步理论
  • 二、接线员“Socket”——连接工具
  • 三、IO流——传输工具
  • 四、项目主体
    • 客户端
      • UI界面
      • NetConn接线员
      • 监听器(控制机制)
      • Video线程
    • 服务器
      • 服务器线程
      • 线程池
  • 五、效果展示

一、TCP/IP初步理论

TCP/IP是指能够在多个不同网络间实现信息传输的协议簇,是网络中最基本的通信协议,其为各种信息的传输制定了“标准”。
如果客户端想要和服务器通信,一方面需要知道服务器映射出来的端口来保证“能连上”,另一方面需要熟悉服务器的接受协议保证“能收到”。
博主的“仿腾讯会议项目”便是建立在这一基础之上实现的。

二、接线员“Socket”——连接工具

Socket也可以称作“套接字”,是两台机器间通信的端点。
常用的构造方法:
Socket(InetAddress address, int port)
address便是你要连接的IP地址,对于本机连接(常用来做测试)的话就是“127.0.0.1”,如果要想连接到其他小伙伴的路由器,需要知道他的对外的外网IP。
port是端口,连接IP地址的一个方式便是连接到该IP地址映射出来的端口,只有端口对应才能连接上。一般我们通过浏览器访问的网址,默认的端口都是80或者8080。

			Socket client=new Socket("127.0.0.1",9999);
			System.out.println("连接成功!");

当然在客户端连之前,需要保证服务器是开着的。通常用ServerSocket创建服务器,并在构造方法中进行端口映射。

           //建立服务器
			ServerSocket ss=new ServerSocket(9999);
			System.out.println("服务器创建成功~!"+port);

那么我们可能会问,我们怎么才知道一个客户端进来了呢?如果我们不知道客户端有没有进来,而是服务器胡乱没有目的的发消息,那必然会出错。这里我们用到了阻塞IO流的方式,来等待客户端进来。

			Socket client=ss.accept();
			System.out.println("有个客户机进来了~!");

accept()方法,阻塞了IO流,在没有客户端进来的时候是不会执行下面的代码,更不能调用服务器的IO流的。
当我们实现上述步骤后,便成功建立了客户端和服务器的连接。

三、IO流——传输工具

接着,我们便要开始传输信息了。其大致思想是获取IO流,使用IO流进行传输。

//获取Socket对象client的IO流
InputStream ins=client.getInputStream();
OutputStreamous=client.getOutputStream();

IO流就比较灵活了,但是通常我们使用数据流DataIO,因为DataIO比较灵活可以直接传整型、字符串等等,省去了用字节流时编码和解码的两步。

//将字节流转换成数据流使得我们用起来更灵活
dins=new DataInputStream(ins);
dous=new DataOutputStream(ous);

下面重点介绍报头的重要性:
消息传输时,实际上是先封包,以数据包的形式进行传输,数据包里装的是编码后的字节表示,但是我们如何判断发过来的是字符串还是图片还是其他的呢,所以这里就需要一个发送一个用来表征的东西,简称“报头”
以发文本消息(字符串)为例:

	public void SendText(String msg)
	{
		//以字节的形式发送文本
		try {
			//报头
			dous.writeByte(3);
			//长度
			byte[] data=msg.getBytes();
			int len=data.length;
			dous.writeInt(len);
			//发送字符串内容
			dous.write(data);
			dous.flush();
			System.out.println("客户机发送字符串len "+len+" msg "+msg);
		}catch(Exception e)
		{
			e.printStackTrace();
			System.out.println("******客户机发送字符串失败");
		}
		
	} 

dous.writeByte(3)发送报头3,在客户端和服务器规定好协议后,服务器在接受新的数据包时,收到3便知道收到的是文本消息,便用文本消息的读取方式去读取,以上便是报头的思想。

四、项目主体

需求:
1、能够传画出来的线条。
2、能够传实时的视频图象。
3、能够传文本消息

客户端

UI界面

①简单地写一个UI界面,有发送按钮、菜单栏、文本框、文本消息显示区域。

    //子菜单栏的构造函数
    public void addJmenuitem(JMenu jm,ClientListener cl,String str)
	{
		JMenuItem jmi=new JMenuItem(str);
		jm.add(jmi);
		jmi.addActionListener(cl);
	}
	public void initUI()
	{
		//界面大小和标题
		this.setSize(900,1000);
		this.setTitle("客户端界面");
		//创建监听器
		ClientListener cl=new ClientListener();
		
        //菜单栏
		JMenuBar jmb=new JMenuBar();
		this.add(jmb,BorderLayout.NORTH);
		//创建菜单
		JMenu jm=new JMenu("功能");
		jmb.add(jm);
		//子菜单
		addJmenuitem(jm,cl,"线条传输");
		addJmenuitem(jm,cl,"视频传输");
		addJmenuitem(jm,cl,"文本传输");
		
		//发送按钮,内容文本框,接受显示多行文本框
		JButton jbu=new JButton("Send");
		this.add(jbu);
		final JTextField jtf=new JTextField(40);
		this.add(jtf);
		JTextArea jta=new JTextArea(20,50);
		this.add(jta);
		this.setLayout(new FlowLayout());
		this.setDefaultCloseOperation(3);
		this.setVisible(true);
		//画笔
		Graphics g=this.getGraphics();
		//创建客户机对象,也就是线程对象.创建视频线程
		final NetConn nc=new NetConn(g,jta);
		Video vi=new Video();
		//将客户机对象和画笔传给监听器
		this.addMouseListener(cl);
		cl.nc=nc;
		cl.g=g;
		cl.vi=vi;
	    //将客户机对象、画笔和flag传给视频线程 
		vi.g=g;
		vi.nc=nc;
		//客户机连接
		if(!nc.conn2Server())
		return;
		nc.start();
		//匿名内部类加监听器
		jbu.addActionListener(new ActionListener(){
			public void actionPerformed(ActionEvent e)
			{ 
				String msg=jtf.getText();
				nc.SendText(msg);
				jtf.setText("");//清空文本框
			}
		});
	}
		public static void main(String args[])
	{
		ShaiZhaiQQ qqClient=new ShaiZhaiQQ();
		qqClient.initUI();
	}
}

NetConn接线员

②写一个接线员NetConn类,用于创建客户端、连接服务器、发送和接收消息。注意由于,接收消息是一直在进行的(只要服务器还活着),所以NetConn显然本质上是一个线程,需要继承Thread,并把接收消息写在run()方法里面。

public class NetConn extends Thread  {
	//指向界面上的多行文本框,发送时需要获取其内容
	private JTextArea jta;
	//画笔
	private Graphics g;
	//多行文本框构造器
	public NetConn(Graphics g,JTextArea jta)
	{
		this.g=g;
		this.jta=jta;
	} 
	//双向,输入输出数据流
	private DataInputStream dins;
	private DataOutputStream dous;
	private ObjectOutputStream oos;
	//发送线条
	public void SendLine(int x1,int y1,int x2,int y2)
	{
	    try {
	    	//报头
	    	dous.writeInt(1);
	    	//四个坐标
	    	dous.writeInt(x1);
	    	dous.writeInt(y1);
	    	dous.writeInt(x2);
	    	dous.writeInt(y2);
	    	System.out.println("客户机发送线条 ("+x1+","+y1+") -> ("+x2+","+y2+")");
	    }catch(Exception e)
	    {
	    	e.printStackTrace();
	    	System.out.println("******客户机发送线条失败");
	    }
	}
	//发送文本消息给服务器
	public void SendText(String msg)
	{
		//以字节的形式发送文本
		try {
			//报头
			dous.writeByte(3);
			//长度
			byte[] data=msg.getBytes();
			int len=data.length;
			dous.writeInt(len);
			//发送字符串内容
			dous.write(data);
			dous.flush();
			System.out.println("客户机发送字符串len "+len+" msg "+msg);
		}catch(Exception e)
		{
			e.printStackTrace();
			System.out.println("******客户机发送字符串失败");
		}
		
	} 
	//发送和接收信息
	public void run()
	{
		try {
			while(true)
			{
				
				//报头
				byte type=dins.readByte();
				if(type==3)
				{
					//读长度
					int len=dins.readInt();
					//读字符串内容并显示在界面上,先读字节再转字符串
					byte[] data=new byte[len];
					dins.read(data);
					String msg=new String(data);
					this.jta.append(msg+"\r\n");
					System.out.println("收到服务器发来的信息"+msg);
					
					
				}
			}
		}catch(Exception e)
		{
			e.printStackTrace();
			System.out.println("*******客户机读消息的线程出错,退出~!");
			
		}
	}
	//判断是否连接成功
	public boolean conn2Server()
	{
		try {
			Socket client=new Socket("127.0.0.1",9999);
			System.out.println("连接成功!");
			//注意这个地方是先获取输入输出流再利用构造器转型成数据流
			InputStream ins=client.getInputStream();
			OutputStream ous=client.getOutputStream();
			this.dins=new DataInputStream(ins);
			this.dous=new DataOutputStream(ous);
			this.oos=new ObjectOutputStream(ous);
			return true;
		}catch(Exception e)
		{
			e.printStackTrace();
			System.out.println("******连接失败~");
			return false;
		}
	}
	

}

这里我把发送视频的代码单独拿了出来。在起初我是用的是RGB色素点进行传输,也就是传每一帧的二维RGB数组,然而这样传输非常非常非常慢!!!(测试了一下,已经到了秒的级别)所以在某位邹姓大佬的帮助下,我get到了用ImageIO的方式进行传输,基本上已经到了毫秒级别。这也说明了“大道至简”:图片直接编码成字节数组,比其他花里胡哨的要快的多!!!

	//发送视频
	public void SendVideo(BufferedImage image)
	{ 
	    try {
	    	//报头
	    	dous.writeInt(2);
	    	//创建image的字节数组
	    	byte[] imageData=null;
	    	
	    		//字节数组输出流
	    		ByteArrayOutputStream baos=new ByteArrayOutputStream();
	    	    ImageIO.write(image, "jpg", baos);
	    	    imageData=baos.toByteArray();
	    	    dous.writeInt(imageData.length);
	    	    dous.write(imageData);

	    }catch(Exception e)
	    {
	    	e.printStackTrace();
	    	System.out.println("******客户机发送视频失败");
	    }
	}

监听器(控制机制)

这块没什么好说的,直接上源码了。

public class ClientListener implements MouseListener,ActionListener {
    //flag表示三种功能
	String flag;
	//获取两个点的坐标 
	public int x1,x2,y1,y2;
	//画笔
	public Graphics g;
	NetConn nc;
    Video vi;
	@Override
	public void actionPerformed(ActionEvent e) {
		// TODO Auto-generated method stub
		flag=e.getActionCommand();
		vi.flag=flag;
		if(flag.equals("视频传输"))
			{vi.start();}
		
	}

	@Override
	public void mouseClicked(MouseEvent e) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void mousePressed(MouseEvent e) {
		// TODO Auto-generated method stub
		if(flag.equals("线条传输"))
		{
			x1=e.getX();
			y1=e.getY();
		}
	}

	@Override
	public void mouseReleased(MouseEvent e) {
		// TODO Auto-generated method stub
		if(flag.equals("线条传输"))
		{
			x2=e.getX();
			y2=e.getY();
			nc.SendLine(x1, y1, x2, y2);
			g.drawLine(x1, y1, x2, y2);
		}
	}

	@Override
	public void mouseEntered(MouseEvent e) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void mouseExited(MouseEvent e) {
		// TODO Auto-generated method stub
		
	}

}

Video线程

摄像头的调用,使用了第三方库Webcam,想解锁更多Webcam功能的可以自行登录官网连接(没有打广告!!!)

public class Video extends Thread {
	//开关
	String flag;
	//画笔
	Graphics g;
	//客户端接线员
	NetConn nc;
	 
	public void run()
	{
		 
		while(true)
	{
		
		if(flag.equals("视频传输"))
		{
			Webcam webcam=Webcam.getDefault();
			webcam.open();
		    BufferedImage buffImage=webcam.getImage();
		    g.drawImage(buffImage, 0, 600, 300, 300,null);
		    //发送视频
		  	nc.SendVideo(buffImage);
		}

		try {
			this.sleep(10);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	}

}

服务器

服务器基本上和客户端没有什么太大的差别,都需要一个基本的UI界面、接线员NetConn(因为你要获取客户端的输入输出流,实现双向传输,可以理解成客户端和服务器是共用输入输出流的)。
区别的话,就是多了两点:1、服务器线程DrawServer。2、线程池(客户端集)

服务器线程

注意while(true)循环让服务器一直开着,可以接收多个客户端。

public class DrawServer extends Thread {
    //消息显示区域
	private JTextArea jta;
	//画笔
	public Graphics g;
	public DrawServer(JTextArea jta,Graphics g)
	{
		this.jta=jta;
		this.g=g;
	}
	public void run()
	{
		setServer(9999);
	}
	public void setServer(int port)
	{
		try {
			//建立服务器
			ServerSocket ss=new ServerSocket(9999);
			System.out.println("服务器创建成功~!"+port);
			//
			while(true)
			{
			Socket client=ss.accept();
			System.out.println("有个客户机进来了~!");
			NetConn nc=new NetConn(client,jta,g);
			nc.start();
			Tools.als.add(nc);
			}	
		}catch(Exception e)
		{
			e.printStackTrace();
			System.out.println("******服务器创建失败");
		}
	}

}

线程池

tell the truth 其实就是一个队列,用来存储连进来的客户端。这里有一个事情,望读者注意,当客户端退出时,一定要及时从队列中删去“死掉”客户端,不然会导致内存泄漏甚至溢出。

public class Tools {

	//私有构造保护,不要给别人创建这个类的对象的机会
	private Tools()
	{
		
	}
	//在服务器端,保存客户机处理线程对象的队列,也就是连接池
	public static ArrayList<NetConn> als=new ArrayList();
	
	//服务器广播消息,所有客户端都能收到的消息
	public static void caseMsg(String msg)
	{
		for(int i=0;i<als.size();i++)
		{
			NetConn nc=als.get(i);
			nc.SendText(msg);
		}
	}
	
}

五、效果展示

本文标签: 腾讯 入门 通信 会议 java