本次我们主要实现了画板之间的通信,客户端画板可以转递一条直线给服务端画板,服务端画板也可以传递一条直线给客户端画板,两端直线显示在相同位置上,效果如下:
涉及到的知识点主要有以下两点:
1,Java Socket通信。
2,int数据和byte数据的转换。
一,Socket通信
1,TCP/IP协议
TCP是一种面向连接的、可靠的、基于字节流的传输层协议,IP是互联网协议,Java Socket就是基于TCP/IP的网络通信。
我们在进行连接时要知道主机地址和端口号,主机地址就类似于知道机器的位置,端口号就类似于该机器上的某个具体插口,只有知道了这两个东西才能实现具体的服务端和客户端的通信。
端口号的范围为0~65535,其中0到1023是系统保留的端口号,我们设置的时候要设置1023之后的端口号。
2,ServerSocket
在Java中如果要使用TCP协议编写服务端的话就要用ServerSocket,ServerSocket负责接收来自客户端的连接请求,并生成与客户端连接的Socket。
3,Socket
Socket的中文释义是套接字,套接字是两台机器间通信的端点。Socket是作为一个通讯端的存在,这个Socke对象如果是自己创建的,那么该类就是客户端,如果是从ServerSocket中拿到的,那该类就是服务端。理论上可以有无数个Socket端来连接ServerSocket端,在ServerSocket端每有一个Socket发来连接请求时,就会创造一个与之相对应的Socket对象,示意如下:
4,InputStream和OutputStream
<1>Socket类可以用来编写客户端,ServerSocket可以用来编写服务端。创建ServerSocket对象时需要绑定一个端口号,这样客户端才可以通过该端口号进行连接通信;创建Socket对象时需要声明一个IP地址和ServerSocket对象的端口号,这样才能对服务端发送连接请求。
<2>当客户端发送连接请求服务端接收了连接请求之后
//客户端 IP地址 端口号
Socket socket = new Socket("192.168.50.117",9050);
//服务端 端口号
ServerSocket sc = new ServerSocket(9050);
Socket socket;
socket = sc.accept();//进行阻塞 得到连接
Socket客户端就会产生输入流和输出流用来接收消息或者发送消息
//客户端
//输出流
OutputStream out = socket.getOutputStream();//得到输出流
//输出流
InputStream in = socket.getInputStream();//得到输出流
ServerSocket创建的Socket对象也会产生输入流和输出流用来接收或者发送消息
//服务端
//输出流
OutputStream out = socket.getOutputStream();//得到输出流
//输出流
InputStream in = socket.getInputStream();//得到输出流
二,int数据和byte数组之间的转换
一个int数据由四个byte组成,由于输入流和输出流中传送的都是字节流,因此要将信息准确地传输就需要对数据进行转化。
1,int转为长度为4的byte数组
假设由四个字节组成的整型数据num从高位到低位依次为:①②③④,定义一个长度为4的字节数组bytes用来存储转换后的数据。
<1>将num数据①②③④向右移24位变成了000①(0代表一个byte的0数据),0xff是二进制的11111111,000①和0xff&操作之后取后8位,变成了000①,强制转为byte类型直接取后8位变成①,赋给byte[0]。
<2>将num数据①②③④向右移16位变成00①②,和0xff&操作之后变成000②,强制转型为byte类型后变成②存入byte[1]中。
依次类推,bytes数组从第0位到第3位依次存入①②③④,代码如下:
//将1个int拆为长度为4的byte数组
public byte[] devide(int num) {
byte[] bytes = new byte[4];
bytes[0]=(byte)((num)>>24&0xff);
bytes[1]=(byte)((num>>16)&0xff);
bytes[2]=(byte)((num>>8)&0xff);
bytes[3]=(byte)((num)&0xff);
return bytes;
}
2,将长度为4的byte数组转为int整数
按照上面的转化结果bytes数组中依次存入了整型数组的①②③④四个字节。
<1>由于函数返回的是int类型,当系统检测到byte类型数据可能要转化成int类型时就会将byte内存的高位自动按符号补位(为了保持二进制补码的一致性),然后将该32位数据和0xff&操作取后八位。byte[0]&0xff之后变成000①,向左移位24位变成①000.
<2>byte[1]&0xff之后变成000②,向左移位16位之后变成0②00,和<1>结果|操作之后变成①②00.
依次类推,最后返回的int数据为①②③④,代码如下:
//将长度为4的byte数组合并为1个int
public int combine(byte[] bytes) {
return (bytes[0]&0xff)<<24
|(bytes[1]&0xff)<<16
|(bytes[2]&0xff)<<8
|(bytes[3]&0xff);
}
三,通信画板的实现
1,客户端和服务端实现连接
客户端:
package com.yzd0319.convertPad;
import java.awt.Graphics;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import javax.swing.JFrame;
public class Client {
//主函数 程序入口
public static void main(String[] args) throws UnknownHostException, IOException {
Client client = new Client();
client.loginClient();
}
//输出流
OutputStream out;
//输出流
InputStream in;
//通信函数 可以用来接收函数
public void loginClient() throws UnknownHostException, IOException {
// IP地址 端口号
Socket socket = new Socket("192.168.50.117",9050);
}
服务端:
package com.yzd0319.convertPad;
import java.awt.Graphics;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import javax.swing.JFrame;
public class Server {
//主函数 程序入口
public static void main(String[] args) throws IOException {
Server server = new Server();
server.loginServer();
}
//输出流
OutputStream out;
//通信函数 主要用来接收数据
public void loginServer() throws IOException {
//Socket socket = new Socket("192.168.50.117",9050);
//ServerSocket负责接收客户连接请求 并生成与客户端连接的Socket
ServerSocket sc = new ServerSocket(9050);
Socket socket;
//不断尝试连接
while(true) {
socket = sc.accept();//得到连接 只需要服务端进行尝试连接就好了 服务端不需要尝试连接 一旦服务端尝试连接成功 服务端和客户端双边都连接成功
out = socket.getOutputStream();//得到输出流
了一个画图板界面
InputStream in = socket.getInputStream();//得到输入流
}
}
}
2,实现画板界面,画一条直线
在这里我们使用匿名内部类和鼠标适配器,只重写鼠标按下和鼠标释放这两个函数。
//界面函数 主要用来发送数据
public Graphics showUI() {
JFrame jf = new JFrame("客户端画板发送");
jf.setSize(300, 400);
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.setVisible(true);
//得到画笔参数
Graphics g = jf.getGraphics();
//绑定鼠标监听器 使用鼠标适配器 匿名内部类
jf.addMouseListener(new MouseAdapter() {
int xDown;//鼠标按下坐标
int yDown;
int xUp;//鼠标释放坐标
int yUp;
//鼠标按下
public void mousePressed(MouseEvent e) {
xDown=e.getX();
yDown=e.getY();
}
//鼠标释放
public void mouseReleased(MouseEvent e) {
xUp=e.getX();
yUp=e.getY();
g.drawLine(xDown, yDown, xUp, yUp);//在鼠标按下和释放的位置画一条直线
}
});
return g;
}
3,一端将直线数据发送出去
定义一个dataLine数组用来存放直线起点和终点坐标,画出直线之后将数组中的数据依次发送出去。
g.drawLine(xDown, yDown, xUp, yUp);//在鼠标按下和释放的位置画一条直线
int[] dataLine = new int[4];//接受直线数据的数组
dataLine[0] = xDown;
dataLine[1] = yDown;
dataLine[2] = xUp;
dataLine[3] = yUp;
for(int i=0;i<dataLine.length;i++) {
try {
out.write(dataLine[i]);
//将1个int型数据转化为byte数组发送出去
out.flush();
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
4,另一端接收直线数据并显示
也同样定义一个dataline数组用来接收直线的四个数据,由于write每次只能读入一个数据,因此定义一个xy暂时存储write方法读到的数据,当全部数据都读取到后再将直线画出
socket = sc.accept();//得到连接 只需要服务端进行尝试连接就好了 服务端不需要尝试连接 一旦服务端尝试连接成功 服务端和客户端双边都连接成功
out = socket.getOutputStream();//得到输出流
InputStream in = socket.getInputStream();//得到输入流
Graphics g = showUI();//得到画笔参数 同时创建了一个画图板界面
int[] dataLine = new int[4];//接受直线数据的数组
int xy;//暂时接受输入流
int index=0;//数据数组下标
while(index<4) {
xy=in.read();//得到输入数据 每次一个
//System.out.println("服务端读取成功");
dataLine[index]=xy;
index++;
while(index==4) {//当四个数据全部读取到的时候 将直线画出
g.drawLine(dataLine[0], dataLine[1], dataLine[2], dataLine[3]);
index=0;//将index重新置为0 这样就能一直读取数据
System.out.println("服务端接收到"+dataLine[0]+" "+dataLine[1]+" "+dataLine[2]+" "+dataLine[3]);
}
}
2、3、4步代码为客户端和服务端相同代码,分别运行客户端和服务端代码得到的效果如下:
我们发现虽然可以实现客户端和服务端两端之间的直线通信,但直线显示的位置却不一致,这是因为以直线起点坐标x1为例,x1是一个整型数据,占4个byte,而write方法发送数据的时候每次只能发送一个byte的数据,read方法读取的时候也只能每次读取一个byte的数据,结合代码具体分析:
//发送数据 将四个点的数据发送出去 一个一个发送
for(int i=0;i<dataLine.length;i++) {
try {
//将int型数据转化为byte数组发送出去
out.write(dataLine[i]);//错误代码 没有进行数字转换 直线显示位置不同
out.flush();//将流中的数据发送出去
//System.out.println("客户端发送成功");
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
//接收数据
while(index<4) {
xy=in.read();//错误代码 没有数据转换 直线位置没显示在同一端
dataLine[index]=xy;
index++;
while(index==4) {//当直线数据全部接收到后将该直线画出
g.drawLine(dataLine[0], dataLine[1], dataLine[2], dataLine[3]);
index=0;//将index重新置为0 这样就能不断接收数据
System.out.println("客户端接收到"+dataLine[0]+" "+dataLine[1]+" "+dataLine[2]+" "+dataLine[3]);
}
}
<1>发送端:以发送整型dataline[0]为例,我们将dataline[0]这个数据的前1个byte用write方法发送出去了,然后就进入i=1的下一次循环了,因此dataline[0]的后3个比特的数据并没有发送出去,和原始数据x1这个四个byte长的数据不一致。
<2>接收端:当调用in.read()方法读取时,只能读1个byte的数据,然后在该方法内部将该byte数据自动转型为int类型(前24位自动补零)赋给dataline[0],将该dataline[0]认为是直线坐标x1.
因此当我们在客户端通过鼠标按下和释放得到的直线数据x1,和我们在客户端发送出去的x1’以及在服务端读到的x1’不一致,所以直线显示的位置不一样。
5,数据转换
在客户端和服务端都添加如下函数:
//将1个int拆为长度为4的byte数组
public byte[] devide(int num) {
byte[] bytes = new byte[4];
bytes[0]=(byte)((num)>>24&0xff);
bytes[1]=(byte)((num>>16)&0xff);
bytes[2]=(byte)((num>>8)&0xff);
bytes[3]=(byte)((num)&0xff);
return bytes;
}
//将长度为4的byte数组合并为1个int
public int combine(byte[] bytes) {
return (bytes[0]&0xff)<<24
|(bytes[1]&0xff)<<16
|(bytes[2]&0xff)<<8
|(bytes[3]&0xff);
}
将发送数据和接收数据的代码修改如下:
//发送数据
//将四个点的数据发送出去 一个一个发送
for(int i=0;i<dataLine.length;i++) {
try {
//将int型数据转化为byte数组发送出去
byte[] tempbytes=devide(dataLine[i]);
out.write(tempbytes);//将数据写入流中
//out.write(dataLine[i]);//错误代码 没有进行数字转换 直线显示位置不同
out.flush();//将流中的数据发送出去
//System.out.println("客户端发送成功");
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
//接收数据
while(index<4) {
//read方法每次读一个字节并自动转化为整型 (0~255)
//一个整型数据由4个byte组成
byte[] bytes = new byte[4];
for(int i=0;i<4;i++) {
//read函数每次读取一个字节并自动转为int
//当int的数据超过255时需要用4个字节表示,如果只读8位信息丢失 因此需要连续读四次 将四个字节全部读出拼成一个长度位4的byte数组
//然后将该byte数组转化为相应的int值
bytes[i]=(byte)in.read();
}
xy=combine(bytes) ;
dataLine[index]=xy;
index++;
while(index==4) {//当直线数据全部接收到后将该直线画出
g.drawLine(dataLine[0], dataLine[1], dataLine[2], dataLine[3]);
index=0;//将index重新置为0 这样就能不断接收数据
System.out.println("客户端接收到"+dataLine[0]+" "+dataLine[1]+" "+dataLine[2]+" "+dataLine[3]);
}
}
<1>发送端:在发送数据的时候将int型数组调用divide()方法转成长度为4的byte类型数组tempbytes数组,调用write(temptypes)可以每次按tempbytes数组的长度发送字节流,即每次一次性将管道中temptypes数据的4个字节全部发送出去了。
<2>接收端:由于read()方法每次只能读取1个byte的数据,而发送端每次发送了4个byte的数据,因此直线每个点的数据如x1需要连续读4次,依次存入bytes数组的4个位置,再调用combine()方法将bytes数组转化为整数存入dataline数组中成为直线坐标x1,然后index++读取第二个数据y1:
//一个整型数据由4个byte组成
byte[] bytes = new byte[4];
for(int i=0;i<4;i++) {
//read函数每次读取一个字节并自动转为int
//当int的数据超过255时需要用4个字节表示,如果只读8位信息丢失 因此需要连续读四次 将四个字节全部读出拼成一个长度位4的byte数组
//然后将该byte数组转化为相应的int值
bytes[i]=(byte)in.read();
}
xy=combine(bytes) ;
dataLine[index]=xy;//得到一个直线坐标数据
index++;//读取直线的下一个坐标
基于以上内容我们就能得到一开始通信画板的效果啦,完整代码如下:
百度网盘链接—提取码:v78e