TCP/IP是指能够在多个不同网络间实现信息传输的协议簇,是网络中最基本的通信协议,其为各种信息的传输制定了“标准”。
如果客户端想要和服务器通信,一方面需要知道服务器映射出来的端口来保证“能连上”,另一方面需要熟悉服务器的接受协议保证“能收到”。
博主的“仿腾讯会议项目”便是建立在这一基础之上实现的。
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流进行传输。
//获取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界面,有发送按钮、菜单栏、文本框、文本消息显示区域。
//子菜单栏的构造函数
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显然本质上是一个线程,需要继承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
}
}
摄像头的调用,使用了第三方库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);
}
}
}