Java Socket通信实现多人多端网络画板聊天室
老规矩,先上实现的效果展示!
Java Socket通信实现多人多端网络画板聊天室
本文介绍了一个基于Socket实现网络画板聊天室的完整过程,聊天室具备多人文本对话、同步绘图等功能。
初尝试
Socket简介
Socket英文原意有插座、插孔的意思,在计算机术语中表示套接字。所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。Socket就像一个邮递员。我们想要将消息发送到哪里,或者从哪里接收消息,都需要通过套接字(Socket)。
Socket库相关API简介
服务端的ServerSocket
服务端在连接中是被动方,它可以被动的接收来自客户端的连接申请。
创建
在socket库中,服务端创建的是ServerSocket类。常用的构造方法是:
ServerSocket sevsoc=new ServerSocket(8888);
因为服务端的IP地址指向的一定是本机,所以可以省略IP地址的参数,只需要声明该SeverSocket绑定的端口即可。每一个端口指向电脑中的一个进程,相当于告诉ServerSocket要把收到的消息发送到哪一个进程上。(注:每一台主机的端口数都是有限的,数量为2^15个,也就是65535个。其中前8000个尽量不要用,以免和系统的程序冲突。)
等待连接
创建ServerSocket对象后,它需要等待Socket的连接申请。这里我们需要用到accept()方法,当ServerSocket对象被创建之后,它会阻塞在accep()方法处,直至接收到客户端Socket的申请。
//等待用户连接
Socket soc=sevsoc.accept();
获取IO流
当ServerSocket获得了Socket的连接申请后,我们需要获取该Socket对象的输入输出流,当我们需要向Socket发送消息时需要用到输出流OutputStream,当我们需要从Socket接收消息时需要用到输入流InputStream。
//获取IO流
InputStream input=soc.getInputStream();
OutputStream output=soc.getOutputStream();
读写数据
通过OutputStream发送数据使用的是write()方法
从上图我们可以看出通过OutputStream只能发送字节数组,那当我们需要发送整数、字符串或者其他更复杂的数据怎么办呢?一个方法是,我们可以使用OutputStream的子类DataOutputStream,DataOutputStream提供了发送int、char、double等数据类型的方法,这里不展开叙述。另一个方法是,我们模拟DataOutputStream写出类似的方法,并添加我们需要的功能。
//发送一个byte数组
byte[]bt=new byte[4];
output.write(bt);
接收数据使用的是read()方法
//使用一个byte数组接收数据(注意,这里的数组长度代表了需要接收的byte个数)
byte[]bt=new byte[4];
input.read(bt);
客户端的Socket
客户端在连接中是主动方,它主动向目标主机发起连接申请。
客户端Socket常用的构造方法如下所示:
这里需要输入的第一个参数是目标主机的IP地址,第二个参数是目标主机上的目标进程的端口号。
后面的输入输出流(IO流)的获取和读写方法与上文介绍ServerSocket时相同,不再赘述。
尝试一次简单的通信
PS:记得先打开服务端再打开客户端,因为服务端是需要等待客户端连接的。
服务端:
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;public class SimpleServer {public static void main(String[]arguments) throws IOException{SimpleServer spsev=new SimpleServer();spsev.test();}private void test() throws IOException {ServerSocket sevsoc=new ServerSocket(8888);System.out.println("服务端已上线!");Socket soc=sevsoc.accept();System.out.println("用户已连接!");//获取IOInputStream input=soc.getInputStream();OutputStream output=soc.getOutputStream();System.out.println("已获取用户的IO");//服务端接收消息//先创建一个用于接收数据的数组,数组长度可以设大一点byte[]bt1=new byte[100];input.read(bt1);//将接收到的数组解析为StringString str1=new String(bt1);System.out.println("接收到:"+str1);//服务端响应消息String str2="客户端你好,我已经接收到了你的消息,我是服务端!";//使用getByte方法可以直接将字符串转为一个byte数组byte[]bt2=str2.getBytes();//然后我们将这个数组发送出去output.write(bt2);input.close();output.close();sevsoc.close();}
}
客户端:
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;public class SimpleClient {public static void main(String[]arguments) throws UnknownHostException, IOException{SimpleClient spcln=new SimpleClient();spcln.test();}private void test() throws UnknownHostException, IOException {/*此处因为是在本地测试,"localhost"代表本机,8888代表888端口如果需要和其他主机连接,"localhost"处替换未为目标主机的IP地址*/Socket soc=new Socket("localhost",8888);System.out.println("Socket已上线!");InputStream input=soc.getInputStream();OutputStream output=soc.getOutputStream();//服务端发送消息String str1="服务端你好,我是客户端!";//使用getByte方法可以直接将字符串转为一个byte数组byte[]bt1=str1.getBytes();//然后我们将这个数组发送出去output.write(bt1);//客户端接收消息//先创建一个用于接收数据的数组,数组长度可以设大一点byte[]bt2=new byte[100];input.read(bt2);//将接收到的数组解析为StringString str2=new String(bt2);System.out.println("接收到:"+str2);input.close();output.close();soc.close();}
}
输出结果:
通过上面这个例子,相信你已经了解了Socket通信的基本方法。那么下面就让我们走进真正的画板聊天室项目吧!
开启项目!
窗体界面代码
下面的代码只是用来测试窗体显示效果的,未加监听器,也暂时未加入通信功能。
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.GridLayout;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;public class TestFrame {private JTextArea jta1;private JTextArea jta2;private Graphics g;public static void main(String[]arguments){TestFrame tf=new TestFrame();tf.showFrame();}private void showFrame() {//创建窗体JFrame jf=new JFrame("客户端");jf.setSize(1000,500);jf.setDefaultCloseOperation(3);jf.setLocationRelativeTo(null);jf.setResizable(false);//设置窗体界面布局为网格布局,设置布局为一行两列GridLayout grid=new GridLayout(1,2);jf.setLayout(grid);//设置窗体显示风格(这一步可以省略)try {UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel");} catch (ClassNotFoundException | InstantiationException | IllegalAccessException| UnsupportedLookAndFeelException e) {e.printStackTrace();}//聊天界面(窗体左边的部分)JPanel jpLeft=new JPanel();JLabel jlb1=new JLabel("消息窗口");jpLeft.add(jlb1);//接收消息框jta1=new JTextArea(9,40);jta1.setLineWrap(true);//设置文本框内容自动换行jta1.setWrapStyleWord(true);//设置文本框内容在单词结束处换行jta1.append("开始聊天吧~");//向消息框内添加文本jta1.setEditable(false);//聊天框内容不可修改//添加滚动条JScrollPane jsp1=new JScrollPane(jta1,JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);jpLeft.add(jsp1);JLabel jlb2=new JLabel("输入窗口");jpLeft.add(jlb2);//发送消息框jta2=new JTextArea(9,40);jta2.setLineWrap(true);//设置消息框内的文本每满一行就自动换行jta2.setWrapStyleWord(true);//设置消息框内文本按单词分隔换行//添加滚动条JScrollPane jsp2=new JScrollPane(jta2,JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);jpLeft.add(jsp2);//发送和取消按钮JButton jb1=new JButton("发送");jpLeft.add(jb1);JButton jb2=new JButton("取消");jpLeft.add(jb2);//绘画界面JPanel jpRight=new JPanel();BorderLayout board=new BorderLayout();jpRight.setLayout(board);//设置绘画界面为板式布局//画板JPanel paintBoard=new JPanel();paintBoard.setBackground(Color.white);//右侧功能按键区JPanel buttonBoard=new JPanel();buttonBoard.setPreferredSize(new Dimension(80,0));//添加功能按钮String[]buttonNames={"直线","圆形","矩形","铅笔"};JButton[]jbtList=new JButton[buttonNames.length];for(int i=0;i<buttonNames.length;i++){jbtList[i]=new JButton(buttonNames[i]);buttonBoard.add(jbtList[i]);}//添加颜色按钮Color[]colors={Color.red,Color.yellow,Color.blue,Color.green,Color.black,Color.white};String[]colorButtonNames={"红","黄","蓝","绿","黑","白"};JButton[]CjbtList=new JButton[colorButtonNames.length];for(int i=0;i<colorButtonNames.length;i++){CjbtList[i]=new JButton();CjbtList[i].setActionCommand(colorButtonNames[i]);CjbtList[i].setBackground(colors[i]);buttonBoard.add(CjbtList[i]);}//将画板和按键功能区添加到右侧容器中jpRight.add(paintBoard,BorderLayout.CENTER);jpRight.add(buttonBoard,BorderLayout.EAST);//将左右两个JPanle添加到窗体上jf.add(jpLeft);jf.add(jpRight);jf.setVisible(true);Listener lis=new Listener();paintBoard.addMouseMotionListener(lis);//获取画笔g=paintBoard.getGraphics();}class Listener implements MouseMotionListener{public void mouseDragged(MouseEvent e) {}public void mouseMoved(MouseEvent e) {}}
}
窗体显示效果:
走向多端通信
在前面的例子中,我们实现的是服务端和客户端的双端通信,那如果我们想要实现多个客户端同时互相传递消息,应该怎么实现呢?
在双端通信的基础上,我们只需要把服务端作为信息中转站,让服务端把接收到的每一个客户端的消息再发给其他所有客户端即可。
我们接下来要做的工作可以简单地概括为:
- 构建自己的“通信协议”,让文本、图形顺利传输。
- 对服务端:在服务端建立while循环不断接受Socket的请求,每次接受Socket请求后就将它的输入输出流存入数组列表中。
- 对服务端:每接受一个Socket请求,就创建一个线程,用于监听该Socket的数据。一旦接收到数据,就循环遍历OutputStream的数组列表,将数据逐次发送给其他所有客户端。
- 对客户端:每打开一个客户端就创建一个线程接收消息,但是客户端不需要额外的线程去执行发送消息的操作,因为监听器在监听到用户操作后可以发送消息。
构建自己的“通信协议”
通信协议是指双方实体完成通信或服务所必须遵循的规则和约定……交流什么、怎样交流及何时交流,都必须遵循某种互相都能接受的规则。这个规则就是通信协议。
本项目实现的通信功能共有:发送文字、发送直线、发送圆形、发送矩形、发送铅笔、更改颜色、清空画布七个功能。每一个功能发送的消息类型都不尽相同,因此我们需要一套通信协议来告诉通信双方,目前传送的是什么消息。
byte数组和int的转换
在上述协议中,我们在很多地方都需要传输int数据类型,比如说发送字符串长度、发送坐标、发送颜色值等。但是我们的输入输出流只能传输byte和byte数组,所以我们需要构建方法完成byte数组和int的转换。
//将int转换为byte数组
public byte[] getByte(int number){byte[]bt=new byte[4];bt[0]=(byte) ((number>>0) & 0xff);bt[1]=(byte) ((number>>8) & 0xff);bt[2]=(byte) ((number>>16) & 0xff);bt[3]=(byte) ((number>>24) & 0xff);return bt;
}//将byte数组还原为int
public int getInt(byte[]bt){int number=(bt[3]& 0xff)<<24|(bt[2]& 0xff)<<16|(bt[1]& 0xff)<<8|(bt[0]& 0xff)<<0;return number;
}
一个int(32bit)数据类型的长度是4个byte(8bit),如果将int强制转型为byte,会丢失int高24位的数据。所以我们需要将int逐次移位,并逐次取低八位的值存入byte,这样才能保证int的完整性。int和byte数组相互转换的具体原理解析,可以参考博主之前发的这篇文章:Java int和byte数组互相转换时为什么要用到&0xff
发送文字
发送
我们在多行文本框中输入我们想要发送的文字内容,点击发送按钮后,消息被发出。我们给发送按钮添加动作监听器,然后程序判断按下的是发送按钮时,执行sendMsg()函数。在执行所有发送消息操作前,都要发送一条4个汉字长度(8byte)的字符串,告诉接收方目前正在发送的消息类型。
//发送消息
public void sendMsg(){try {String OP="发送文字";//表示发送文字操作(一定要是8个字节长度的操作)output.write(OP.getBytes());//发送用户名(长度为4个字节)output.write(clientName.getBytes());//获得发送文本的字节长度int msglen=jta2.getText().getBytes().length;//发送字节长度(方便接收方定义用于接收数据的byte数组大小)output.write(getByte(msglen));//发送文本内容output.write(jta2.getText().getBytes());System.out.println("发送:"+jta2.getText());//获得当前时间SimpleDateFormat formatter= new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z");Date date = new Date(System.currentTimeMillis());//在多行文本框中显示发送的消息jta1.append("\n\r"+"我 "+formatter.format(date)+"\n\r"+jta2.getText());//将滚动条拖到最下方jta1.setCaretPosition(jta1.getText().length());//清空输入框的文本内容jta2.setText("");} catch (IOException e1) {e1.printStackTrace();}
}
接收
在所有消息类型进行接收前,都要进行这一步操作:
//接收代表消息类型的字符串,确认接收到的是什么类型的消息
byte[]OP=new byte[8];
input.read(OP);
//然后根据这个字符串跳转到相应的读取方法
switch(new String(OP)){……
}
//接收文字的方法
private void readMsg() {try {//接收消息发送者编号byte[]otherName=new byte[4];input.read(otherName);//接收发送文本内容的长度byte[]bt1=new byte[4];input.read(bt1);int reclen=getInt(bt1);System.out.println("receive byte:"+reclen);//根据接收到的文本内容字节长度创建用于接收消息的byte数组byte[]bt2=new byte[reclen];//接收文本内容input.read(bt2);String recmsg=new String(bt2);System.out.println("接收到:"+recmsg);//获得当前时间SimpleDateFormat formatter= new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z");Date date = new Date(System.currentTimeMillis());//将接收的信息加入多行文本框jta1.append("\n\r"+"用户"+new String(otherName)+" "+formatter.format(date)+"\n\r"+recmsg);//将滚动条拖到最下方jta1.setCaretPosition(jta1.getText().length());} catch (IOException e) {e.printStackTrace();}
}
发送图形
因为我们想要实现的效果是,同步画板的画面。所以说每次在画布上画完一个图形就需要执行一次发送图形的操作。
比如说当我们绘制直线,在监听到鼠标按下+鼠标松开操作后,就需要在画板上绘制一条直线,并发送一次图形消息。
绘制图形并发送
下面代码中用到的shapePoint是一个长度为4的int数组
//MouseListener中的方法
public void mousePressed(MouseEvent e) {//记录鼠标按下的坐标shapePoint[0]=e.getX();shapePoint[1]=e.getY();
}public void mouseReleased(MouseEvent e) {//记录鼠标松开的坐标shapePoint[2]=e.getX();shapePoint[3]=e.getY();//判断最后按下的是哪个图形按钮switch(nowButton){case"直线":System.out.println("直线"+shapePoint[0]+" "+shapePoint[1]+" "+shapePoint[2]+" "+shapePoint[3]);//绘制直线g.drawLine(shapePoint[0],shapePoint[1],shapePoint[2],shapePoint[3]);//调用发送图形方法sendShape();break;case"圆形":System.out.println("圆形"+shapePoint[0]+" "+shapePoint[1]+" "+shapePoint[2]+" "+shapePoint[3]);//记录圆形左上角坐标点,并计算其宽高int x1=Math.min(shapePoint[0],shapePoint[2]);int y1=Math.min(shapePoint[1],shapePoint[3]);int width=Math.abs(shapePoint[0]-shapePoint[2]);int height=Math.abs(shapePoint[1]-shapePoint[3]);shapePoint[0]=x1;shapePoint[1]=y1;shapePoint[2]=width;shapePoint[3]=height;//绘制椭圆g.fillOval(shapePoint[0],shapePoint[1],shapePoint[2],shapePoint[3]);//调用发送图形方法sendShape();break;case"矩形"://实现方法与画圆类似System.out.println("矩形"+shapePoint[0]+" "+shapePoint[1]+" "+shapePoint[2]+" "+shapePoint[3]);x1=Math.min(shapePoint[0],shapePoint[2]);y1=Math.min(shapePoint[1],shapePoint[3]);width=Math.abs(shapePoint[0]-shapePoint[2]);height=Math.abs(shapePoint[1]-shapePoint[3]);shapePoint[0]=x1;shapePoint[1]=y1;shapePoint[2]=width;shapePoint[3]=height;g.fillRect(shapePoint[0],shapePoint[1],shapePoint[2],shapePoint[3]);sendShape();break;}
}//绘制铅笔的方法
public void mouseDragged(MouseEvent e) {if(nowButton.equals("铅笔")){shapePoint[2]=shapePoint[0];shapePoint[3]=shapePoint[1];shapePoint[0]=e.getX();shapePoint[1]=e.getY();g.drawLine(shapePoint[0],shapePoint[1],shapePoint[2],shapePoint[3]);sendShape();}
}
接收
//接收图形消息
private void readShape(String OP) {//只要是传输两个点的图形绘制操作都可以用这一条try {//更新两个点的坐标for(int i=0;i<4;i++){byte[]bt=new byte[4];input.read(bt);shapePoint[i]=getInt(bt);}switch(OP){case"发送直线":g.drawLine(shapePoint[0],shapePoint[1],shapePoint[2],shapePoint[3]);break;case"发送圆形":g.fillOval(shapePoint[0],shapePoint[1],shapePoint[2],shapePoint[3]);break;case"发送矩形":g.fillRect(shapePoint[0],shapePoint[1],shapePoint[2],shapePoint[3]);break;case"发送铅笔":g.drawLine(shapePoint[0],shapePoint[1],shapePoint[2],shapePoint[3]);break;}} catch (IOException e) {e.printStackTrace();}
}
更改颜色
发送
//发送颜色的哈希值
private void sendColor(int colorHashCode) {try {String OP="更改颜色";output.write(OP.getBytes());//发送颜色哈希值output.write(getByte(colorHashCode));} catch (IOException e1) {e1.printStackTrace();}
}
接收
//更改颜色
private void changeColor() {try {byte[]color=new byte[4];input.read(color);g.setColor(new Color(getInt(color)));} catch (IOException e) {e.printStackTrace();}
}
清空画布
发送
//只需要发送一个字符串的操作
private void sendSimpleOP(String OP) {try {output.write(OP.getBytes());} catch (IOException e1) {e1.printStackTrace();}
}
接收
//只接收一个字符串的简单操作
private void readSimpleOP(String OP) {switch(OP){case"清空画布":paintBoard.paint(g);break;}
}
使用多线程收发消息
服务端的消息通道线程工作流程
这里的MessageChannel可能需要再讲一下,怎么将接收到的消息发送给每一个客户端呢?
前面说到SeverSocket每接收一个Socket的申请,就将从它获取的输入输出流对象存放到一个数组列表里。当MessageChannel每接收到一次来自客户端的消息时,就循环遍历存放输出流的数组列表,然后依次将对应的消息发送给每一个客户端。
那么所有的代码原理差不多都介绍完了,直接放源代码吧!
完整源代码链接 提取码:swue
试着运行一下!
先打开服务端,接着多次打开客户端,然后就可以顺利通信了!
Java Socket通信实现多人多端网络画板聊天室相关推荐
- AgileEAS.NET SOA 中间件平台.Net Socket通信框架-完整应用例子-在线聊天室系统-代码解析...
一.AgileEAS.NET SOA中间件Socket/Tcp框架介绍 在文章AgileEAS.NET SOA 中间件平台.Net Socket通信框架-介绍一文之中我们对AgileEAS.NET S ...
- 使用java socket实现一个简单的一对多聊天室
socket就是指两个应用程序之间通信的抽象对象,我们可以使用socket实现网络应用程序.例如一个多人聊天室. 目录 先从服务端开始 创建一个窗口类 创建一些方法,用于管理服务端链接,或者进行消息的 ...
- Java进阶:基于TCP的网络实时聊天室(socket通信案例)
目录 开门见山 一.数据结构Map 二.保证线程安全 三.群聊核心方法 四.聊天室具体设计 0.用户登录服务器 1.查看当前上线用户 2.群聊 3.私信 4.退出当前聊天状态 5.离线 6.查看帮助 ...
- flex java socket通信
引用:http://developer.51cto.com/art/201003/189791.htm Java socket通信如何进行相关问题的解答呢?还是需要我们不断的学习,在学习的过程中会遇到 ...
- Java Socket实现简易多人聊天室传输聊天内容或文件
Java Socket实现简易多人聊天室传输聊天内容或文件 Java小练手项目:用Java Socket实现多人聊天室,聊天室功能包括传输聊天内容或者文件.相比于其它的聊天室,增加了传输文件的功能供参 ...
- Java Socket通信之TCP协议
文章目录 一. Java流套接字通信模型 1.TCP模型 2.TCP Socket常见API ServerSocket API Socket API 二.TCP流套接字编程 1.回显服务器 2.多线程 ...
- 使用socket.io做一个简单的WEB聊天室
使用socket.io做一个简单的WEB聊天室(可消息私发) 1. 创建一个空的工程目录 空的目录命名为chat-web 2. 创建package.json 使用命令:npm init,会引导你设置p ...
- java Socket通信(一)
ava socket通信已经被封装好了主要使用两个类ServerSocket 和Socket 首先写一个1v1的通信 服务端 /** * */ package com.dnion.socket; im ...
- java:socket通信
基于tcp协议,建立稳定连接的点对点的通信. 实时,快速,安全性高,占用系统资源高,效率低 请求-响应模式(request, response) 客户端: 在网络通讯中,第一次主动发起通讯的程序叫做客 ...
最新文章
- ACM1881 01背包问题应用
- PHP如何防止XSS攻击
- SQL连接,Oracle关联加号(+)等相关知识
- 二维数组的地址表达方式
- MM 收货容差如何设定
- php生成不重复时间戳,PHP获取时间戳和微秒数以及生成唯一ID
- ASP.NET MVC PartialView用法
- linux系统python的版本怎么升级,python---linux下升级python的版本
- xrdp协议_XRDP与VNC的关系(转载)
- hadoop hive集群_Hive的优化和压缩
- 通过Chrome扩展来批量复制知乎好友
- 第十章 嵌入式linux的调试技术
- php $_SERVER详细参数解析
- 程序员生存定律--如何尽快变的稍微专业一点
- Hutool PinyinException: No pinyin jar found Please add one of it to your project问题解决
- matlab传递闭包算法,传递闭包(用关系矩阵求传递闭包怎么求)
- 从“杀猪盘”到杀洋盘,短信里藏了多少套路?
- MFC CString互转LPVOID
- 中国 vs 卡塔尔 一场幸运的比赛
- 基金使用计划 数学建模 matlab,基金使用计划模型