Java第十三课——客户端与服务器单线多样化数据传输
之前我们完成了基本的服务器和客户端的额雏形,能实现单字节的发送,那么接下来就需要尝试多样化的数据传输。毕竟在我们日常生活中,聊天不可能每次都发送一个字节,我们所使用和汉字都是char(两个字节),那么如何来完成多样化传输,来发送大量文字,文件,视频呢?
这节课内容都按上节课的基础上来改动
服务器端:
public class Server {
ServerSocket serversocket;
public void create(){
try{
serversocket = new ServerSocket(9876);
System.out.println("ServerSocket: loading successfully");
} catch (IOException e){
System.out.println("ServerSocket: loading error");
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(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) {
// TODO Auto-generated method stub
UserSocket us = new UserSocket();
us.userUI();
us.conn();
}
}
一、你画我猜
平时我们玩的你画我猜,就是在滑画板上画图,至于在画板上如何实现画图就很简单了,可以参考我前面的文章
这里面写了基本的划线和画圆,但你画我猜就是在基本画图板的基础上去扩展,在这里就不多重复了,那么这次我们要完成的核心便是数据的传输,先来看看单线通信下事件发生的次序:
客户端:在界面上画图——>监听器获取到轨迹,记录坐标——>发送出去
服务器:打开一个窗体——>获取到坐标——>在窗体上按照坐标再画出来
下面以画直线来举例:
1、从事件的发生顺序来写,那么首先客户端需要一个新的界面和Graphics
public void drawLineUI(){
JFrame draw = new JFrame();
draw.setSize(500, 500);
draw.setTitle("你画我猜——客户端");
draw.setLocationRelativeTo(null);
draw.setDefaultCloseOperation(3);
draw.setVisible(true);
Graphics g = draw.getGraphics();
}
然后便是监听器MouseListener获取轨迹(为了减少篇幅总体长度,我用了MouseAdapter而没有创建一个新的类,建议写个新的类,实现ActionListener和MouseListener两个接口)
public void drawLineUI(){
JFrame draw = new JFrame();
draw.setSize(500, 500);
draw.setTitle("你画我猜——客户端");
draw.setLocationRelativeTo(null);
draw.setDefaultCloseOperation(3);
draw.setVisible(true);
Graphics g = draw.getGraphics();
MouseListener l = new MouseAdapter() {
int x1, y1, x2, y2;
@Override
public void mousePressed(MouseEvent e) {
// TODO Auto-generated method stub
super.mousePressed(e);
x1 = e.getX();
y1 = e.getY();
}
@Override
public void mouseReleased(MouseEvent e) {
// TODO Auto-generated method stub
super.mouseReleased(e);
x2 = e.getX();
y2 = e.getY();
}
};
draw.addMouseListener(l);
}
接着从客户端把数据发送出去,但要注意的是发送出去的顺序,以怎样的顺序发送出去,就需要以怎样的顺序接收数据,这也就是通信协议,是自己规定的,是客户端和服务器之间通信的标准
@Override
public void mouseReleased(MouseEvent e) {
// TODO Auto-generated method stub
super.mouseReleased(e);
x2 = e.getX();
y2 = e.getY();
//OutputStream out是全局变量
try {
out.write(x1);
//输出以确认是否发成功
System.out.println("user: has writed: x1=" + x1);
out.write(y1);
System.out.println("user: has writed: y1=" + y1);
out.write(x2);
System.out.println("user: has writed: x2=" + x2);
out.write(y2);
System.out.println("user: has writed: y2=" + y2);
System.out.println("————————————————————————————");//分割线
//记得也要画条线
g.drawLine(x1, y1, x2, y2);
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
服务器打开一个新窗体
public void drawLineUI(){
JFrame draw = new JFrame();
draw.setSize(400, 400);
draw.setTitle("你画我猜——服务器端");
draw.setLocationRelativeTo(null);
draw.setDefaultCloseOperation(3);
draw.setVisible(true);
//Graphics g设为全局变量
g = draw.getGraphics();
}
接收数据并画图,要注意的是,服务器不可能只是接收一次数据,画一条线就结束了,应该是不停的接收数据画图,所以要加上while(true)
public void conn(){
try {
//等待用户连接
Socket socket = serversocket.accept();
System.out.println("ServerSocket: user enter");
//获取输入输出流
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
//读取数据
//先设置int x1,y1,x2,y2的全局变量来接收发过来的数据
while(true){
//按照顺序接收
x1 = in.read();
System.out.println("ServerSocket: has read: x1=" + x1);
y1 = in.read();
System.out.println("ServerSocket: has read: y1=" + y1);
x2 = in.read();
System.out.println("ServerSocket: has read: x2=" + x2);
y2 = in.read();
System.out.println("ServerSocket: has read: y2=" + y2);
System.out.println("————————————————————————————");//分割线
//画线
g.drawLine(x1, y1, x2, y2);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
在启动服务器客户端之前要记得修改主方法里要调用的方法和顺序
服务器:
public static void main(String[] args){
Server server = new Server();
server.create();
server.drawLineUI();
server.conn();
}
客户端:
public static void main(String[] args) {
UserSocket us = new UserSocket();
us.drawLineUI();
us.conn();
}
试试效果!!
——————————————————————————————————————
如果试了效果会发现出现的问题
左边的两条线还正常,右边的线就跑偏了,接下来看看输出
客户端的输出:
服务器端的输出
那么不难看出发送的数据和接收的数据有出入,并且有问题的数据与原数据的差值是256,256正好是一个byte所能表示的数字范围,于是来看看out.write(int b)方法的源码:
上节课也说过,write方法每次只写一个字节,所以要传大于256的数字就需要换发送方法。那么会用到另一个输入输出流,可以很好的解决这个问题:DataOutputStream和DataInputStream,可以发送Int,String,long,float等等类型:
DataOutputStream dout = new DataOutputStream(OutputStream out);
DataInputStream din = new DataInputStream(InputStream in)
于是在客户端:
@Override
public void mouseReleased(MouseEvent e) {
// TODO Auto-generated method stub
super.mouseReleased(e);
x2 = e.getX();
y2 = e.getY();
try {
//变成了writeInt方法
dout.writeInt(x1);
System.out.println("user: has writed: x1=" + x1);
dout.writeInt(y1);
System.out.println("user: has writed: y1=" + y1);
dout.writeInt(x2);
System.out.println("user: has writed: x2=" + x2);
dout.writeInt(y2);
System.out.println("user: has writed: y2=" + y2);
System.out.println("————————————————————————————");//分割线
g.drawLine(x1, y1, x2, y2);
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
服务器端:
public void conn(){
try {
//等待用户连接
Socket socket = serversocket.accept();
System.out.println("ServerSocket: user enter");
//获取输入输出流
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
//换成DataInputStream
DataInputStream din = new DataInputStream(in);
//读取数据
//先设置int x1,y1,x2,y2的全局变量来接收发过来的数据
while(true){
//按照顺序接收
//方法变成了readInt
x1 = din.readInt();
System.out.println("ServerSocket: has read: x1=" + x1);
y1 = din.readInt();
System.out.println("ServerSocket: has read: y1=" + y1);
x2 = din.readInt();
System.out.println("ServerSocket: has read: x2=" + x2);
y2 = din.readInt();
System.out.println("ServerSocket: has read: y2=" + y2);
System.out.println("————————————————————————————");//分割线
g.drawLine(x1, y1, x2, y2);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
再测试一下,就发现可以了
二、迷你视频
依据输入输出流来实现一个视频会议的效果,会用到webcam的jar包用来获取视频,链接如下:
下载之后导包,这个就不发教程了,不会的可以直接去首页搜。
视频的播放其实就是一张一张图片连续播放,所以可以说视频的本质就是图片
先来缕一缕单线视频聊天事件发生次序:
客户端:获取图片——>把图片转成一个一个的像素点——>发送数据
服务器:接收数据——>把像素点转换成图片——>画出图片
首先便是获取图片
在上面那个网址上找到最便捷的图片获取方式:
// get default webcam and open it
Webcam webcam = Webcam.getDefault();
webcam.open();
// get image
BufferedImage image = webcam.getImage();
视频的实现就是加上while(true)然后用Graphics画出来:
在客户端写一个界面,用于获取视频和呈现视频
public void videoUI(){
JFrame video = new JFrame();
video.setSize(300, 300);
video.setTitle("迷你视频——客户端");
video.setLocationRelativeTo(null);
video.setDefaultCloseOperation(3);
video.setVisible(true);
//把Graphics g设成全局变量
g = video.getGraphics();
}
获取视频
public void video(){
// get default webcam and open it
Webcam webcam = Webcam.getDefault();
webcam.open();
// get image
while(true){
BufferedImage image = webcam.getImage();
g.drawImage(image, 50, 50, null);
}
}
到这里可以先启动一下,看看视频能不能成功的画在界面上(这很重要),测试的时候别忘记改main方法里调用的方法
然后就是把图片转换成像素点,每转成一个就发出去一个
但发送时就需要注意,如何在接收时知道,这个像素的颜色应该在哪个位置。比如说我们获取的图片是200×200的,那在客户端这边就写两个for(int i = 0; i < 200; i++),但在服务器该这么写for循环?毕竟服务器并不知道图片的大小
这里先获取服务器的长和宽,然后先发送长和宽,然后用image.getRGB获取像素点对应的颜色,发送出去
public void video(){
// get default webcam and open it
Webcam webcam = Webcam.getDefault();
webcam.open();
// get image
while(true){
BufferedImage image = webcam.getImage();
g.drawImage(image, 50, 50, null);
//获取长和宽
int w = image.getWidth();
int h = image.getHeight();
try {
//先发送长和宽
dout.writeInt(w);
dout.writeInt(h);
//把图片转换
for(int i = 0; i < w; i++){
for(int j = 0; j < h; j++){
//用value记录像素点的颜色
int value = image.getRGB(i, j);
dout.writeInt(value);
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
在这里用了DataOutputStream,所以必须要先连接服务器再打开视频
另外,如果像保存视频,可以用二维数组记录下保存的视频,写进文件里,再另外可以写一个打开文件并画出的视频程序,就实现视频保存了
服务器端:需要一个从窗体来呈现视频
public void videoUI(){
JFrame video = new JFrame();
video.setSize(300, 300);
video.setTitle("迷你视频——服务器端");
video.setLocationRelativeTo(null);
video.setDefaultCloseOperation(3);
video.setVisible(true);
g = video.getGraphics();
}
窗体可以在连接上客户端之后打开
然后便是依据协议接收图片数据并画出,画点时用划线的方法,把起点终点定为同一点即可
public void conn(){
try {
//等待用户连接
Socket socket = serversocket.accept();
System.out.println("ServerSocket: user enter");
//获取输入输出流
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
DataInputStream din = new DataInputStream(in);
//打开窗体
drawLineUI();
//读取数据
while(true){
//先读取长和宽
int w = din.readInt();
int h = din.readInt();
//然后就是读取图片像素点的数据
for(int i = 0; i < w; i++){
for(int j = 0; j < h; j++){
int value= din.readInt();
Color color = new Color(value);
g.setColor(color);
g.drawLine(i, j, i, j);
}
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
最后修改一下两边的主方法
客户端:
public static void main(String[] args) {
UserSocket us = new UserSocket();
us.videoUI();
//一定要先连接,再通视频
us.conn();
us.video();
}
服务器端:
public static void main(String[] args){
Server server = new Server();
server.create();
server.conn();
}
最后启动一下,就实现了视频的通话,但可以看出视频非常卡顿,可以加入缓存或添加压缩方法来使视频更流畅。另外目前只是单向的通信,下节课来实现双向的交流,就可以完成群聊等效果