【案例9-2】 模拟微信聊天
【案例介绍】
**1.**案例描述
在如今,微信聊天已经人们生活中必不可少的重要组成部分,人们的交流很多都是通过微信来进行的。本案例要求:将多线程与UDP通信相关知识结合,模拟实现微信聊天小程序。通过监听指定的端口号、目标IP地址和目标端口号,实现消息的发送和接收功能,并显示聊天的内容。
**2.**运行结果
【案例目标】
【案例分析】
(1)第一要知道用什么技术实现,通过上述任务描述可知此任务是使用多线程与UDP通信相关知识实现的。要实现图中的聊天窗口界面。首先需要定义一个实现微信聊天功能的类,类中需要定义访问微信聊天的输出语句,从而获取输入的发送端端口号、接收端端口号以及实现发送和接收功能的方法。
(2)实现发送数据的功能。该功能通过一个实现了Runnable接口的类实现,类中需要定义获取发送数据的端口号,并在实现run()的方法中,编写发送数据的方法。
(3)实现接收数据的功能。该功能通过一个实现了Runnable接口的类实现,类中需要定义获取接收数据的端口号,并在实现run()的方法中,编写显示接收到的数据的方法。
(4)创建完所有的类与方法后,运行两次程序,同时开启两个窗口来实现聊天功能。
【案例实现】
(1)创建微信聊天程序,其代码具体如下所示。
Room.java
import java.util.Scanner;
public class Room {
public static void main(String[] args) {
System.out.println("微信聊天欢迎您!");
Scanner sc = new Scanner(System.in);
System.out.print("请输入您的微信号登录:");
int sendPort = sc.nextInt();
System.out.print("请输入您要发送消息的微信号:");
int receivePort = sc.nextInt();
System.out.println("微信聊天系统启动!!");
//发送操作
new Thread(new SendTask(sendPort), "发送端任务").start();
//接收操作
new Thread(new ReceiveTask(receivePort), "接收端任务").start();
}
}
上述代码中,第12行代码用多线程实现发送端口号以及实现发送端功能的方法。第14行代码用多线程实现接收端口号以及实现接收端功能的方法。
(2)创建发送数据的任务类,其代码如下所示。
SendTask.java
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;
public class SendTask implements Runnable {
private int sendPort; // 发数据的端口号
// 构造方法
public SendTask(int sendPort) {
this.sendPort = sendPort;
}
@Override
public void run() {
try {
// 1. 创建DatagramSocket对象
DatagramSocket ds = new DatagramSocket();
// 2.输入要发送的数据
Scanner sc = new Scanner(System.in);
while (true) {
String data = sc.nextLine();// 获取键盘输入的数据
// 3.封装数据到 DatagramPacket对象中
byte[] buf = data.getBytes();
DatagramPacket dp = new DatagramPacket(buf, buf.length,
InetAddress.getByName("127.0.0.255"),sendPort);
// 4.发送数据
ds.send(dp);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
上述代码中,第6~10
行代码声明了一个名称为sendPort的变量表示发送数据的端口号,并通过该类的构造方法获取所输入的端口号。第12~29行代码,实现run()方法,在run()方法中,首先创建了DatagramSocket对象,然后通过Scanner对象和循环方法获取键盘输入的数据,并将获取的数据封装到了DatagramPacket对象中,最后通过DatagramSocket对象的send()方法发送数据。在循环方法中,由于发送数据时可能多次连续发送,所以将循环条件设置为true,表示不断循环获取和发送数据。在创建DatagramPacket对象时,为了让当前局域网的所有人都接收到消息,所以将IP地址设置为127.0.0.255。
(3)创建接收数据的任务类,其代码如下所示。
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class ReceiveTask implements Runnable{
private int receivePort;// 接收数据的端口号
public ReceiveTask(int receivePort) {
this.receivePort = receivePort;
}
@Override
public void run() {
try {
// 1.DatagramSocket对象
DatagramSocket ds = new DatagramSocket(receivePort);
// 2.创建DatagramPacket对象
byte[] buf = new byte[1024];
DatagramPacket dp = new DatagramPacket(buf, buf.length);
// 3.接收数据
while (true) {
ds.receive(dp);
// 4.显示接收到的数据
String str = new String(dp.getData(), 0,
dp.getLength());
System.out.println("收到" +
dp.getAddress().getHostAddress()
+ "--发送的数据--" + str);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
上述代码中,第4行代码声明了一个名称为receivePort的变量来表示接收数据的端口号,第5~7
行代码定义一个该类的构造方法,并通过该类的构造方法获取所输入的端口号,第9~28行代码在run()方法中,首先创建了DatagramSocket对象,创建该对象时需要传入接收数据的端口号,然后创建用于接收数据的DatagramPacket对象,接下来通过循环的方法来接收数据,最后通过输出语句来显示接收到的数据。
【案例9-3】 字符串反转
【案例介绍】
1.案例描述
在使用软件或浏览网页时,总会查询一些数据,查询数据的过程其实就是客户端与服务器交互的过程。用户(客户端)将查询信息发送给服务器,服务器接收到查询消息后进行处理,将查询结果返回给用户(客户端)。本案例要求编写一个模拟客户端与服务端交互的程序,客户端向服务器传递一个字符串(键盘录入),服务器将字符串反转后写回,客户端再次接收到的是反转后的字符串。本案例要求使用多线程与TCP通信相关知识完成数据交互。
2.运行结果
【案例目标】
【案例分析】
(1)根据任务描述可以知道该程序用TCP通信技术实现,所以第一条就是定义客户端,键盘录入数据定义Scanner来实现,然后创建客户端指定IP地址和端口号,之后获取输出流,与输入流,最后将字符串写到服务器并将反转后的结果读出来打印在控制台。
(2)实现服务端的代码编写,首先创建服务端绑定客户端的端口号,并用Server的accept()方法接受客户端的请求。
(3)服务端定义run()方法实现之后获取输入输出流,将客户端发送过来的数据读取出来并采用链式编程的思想将字符串反转后返回到客户端。
【案例实现】
(1)创建客户端,用于录入输入的数据。其代码具体如下所示。
Client.java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;
public class client {
public static void main(String[] args) throws UnknownHostException,
IOException {
//创建键盘录如对象
Scanner sc = new Scanner(System.in);
//创建客户端,指定ip地址和端口号
Socket socket = new Socket("127.0.0.1", 54321);
BufferedReader br = new BufferedReader(new
InputStreamReader(socket.getInputStream())); //获取输入流
//获取输出流
PrintStream ps = new PrintStream(socket.getOutputStream());
//将字符串写到服务器去
ps.println(sc.nextLine());
System.out.println(br.readLine()); //将反转后的结果读出来
socket.close();
}
}
上述代码中,第14行代码创建客户端指定IP地址与端口号,第15~18行代码获取输入与输出流,第20行代码用于将字符串写到服务器中去。第21行代码用于将反转后的结果读取出来。
(2)创建服务端实现将客户端数据反转并返回到客户端,其代码如下所示。
Server.java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
public class server
{
public static void main(String[] args) throws IOException
{
ServerSocket server = new ServerSocket(54321);
System.out.println("服务器启动,绑定54321端口");
while(true)
{
final Socket socket = server.accept(); //接受客户端的请求
new Thread() //开启一条线程
{
public void run()
{
try
{
BufferedReader br = new BufferedReader(new InputStreamReader socket.getInputStream()); //获取输入流
PrintStream ps = new PrintStream(socket.getOutputStream());//获取输出流
//将客户端写过来的数据读取出来
String line = br.readLine()
line = new StringBuilder(line).reverse().toString(); //链式编程
ps.println(line); //反转后写回去
socket.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
} .start();
}
}
}
上述代码中,第9行代码用于创建Server服务器绑定端口号。第12行代码用Server的accept()方法来接收客户端的请求。第22行代码用readLine()方法将客户端写过来的数据读取出来,第23~24行代码用链式编程的方式将字符串反转,第25行代码将反转后的字符串返回给客户端打印。
【案例9-4】 客户端向服务端上传文件
【案例介绍】
1.案例描述
编写一个客户端向服务端上传文件的程序,要求使用TCP通信的的知识,完成将本地机器输入的路径下的文件上传到D盘中名称为upload的文件夹中。并把客户端的IP地址加上count标识作为上传后文件的文件名,即IP(count)的形式。其中,count随着文件的增多而增大,例如127.0.0.(1).jpg、127.0.0.(2).jpg。
2.效果显示
上传文件之前
上传文件之后
【案例目标】
【案例分析】
(1)根据任务描述中使用TCP通信的知识实现文件上传功能可知,要实现此功能,需要定义一个服务器接收文件的程序和 一个客户端上传文件的程序。
(2)首先要编写服务器端程序来接收文件。服务器端需要使用ServerSocket对象的accept()方法接收客户端的请求,由于一个服务器可能对于多个客户端,所以当客户端与服务器端简历连接后,服务器需要单独开启一个新的线程来处理与客户端的交互,这时需要在服务器端编写开启新线程的方法。在新线程的方法中,需要获取客户端的端口号,并且使用输入输出流来传输文件到指定的目录中。
(3)编写客户端的功能代码,客户端功能的实现,因为是用户自己输入上传文件。所以要定义键盘录入。录入后需要使用Socket类来创建客户对象,并通过输入输出流来定义指定的文件。
(4)最后我们启动程序,先启动服务端程序,再运行客户端程序来测试上传的结果。
【案例实现】
(1)首先编写服务器端的程序,用来接收文件,其代码具体如下所示。
FileServer.java
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class FileServer
{
public static void main(String[] args) throws Exception
{
//创建ServerSocket对象
ServerSocket serverSocket = new ServerSocket(10001);
while (true)
{
// 调用accept()方法接收客户端请求,得到Socket对象
Socket s = serverSocket.accept();
// 每当和客户端建立Socket连接后,单独开启一个线程处理和客户端的交互
new Thread(new ServerThread(s)).start();
}
}
}
class ServerThread implements Runnable
{
// 持有一个Socket类型的属性
private Socket socket;
// 构造方法中把Socket对象作为实参传入
public ServerThread(Socket socket)
{
this.socket = socket;
}
public void run()
{
// 获取客户端的IP地址
String ip = socket.getInetAddress().getHostAddress();
// 上传图片个数
int count = 1;
try
{
InputStream in = socket.getInputStream();
// 创建上传图片目录的File对象
File parentFile = new File("D:\\upload\\");
// 如果不存在,就创建这个目录
if (!parentFile.exists())
{
parentFile.mkdir();
}
// 把客户端的IP地址作为上传文件的文件名
File file = new File(parentFile, ip + "(" + count +").jpg");
while (file.exists())
{
// 如果文件名存在,则把count++
file = new File(parentFile, ip + "(" + (count++) +").jpg");
}
// 创建FileOutputStream对象
FileOutputStream fos = new FileOutputStream(file);
// 定义一个字节数组
byte[] buf = new byte[1024];
// 定义一个int类型的变量len,初始值为0
int len = 0;
// 循环读取数据
while ((len = in.read(buf)) != -1)
{
fos.write(buf, 0, len);
}
// 获取服务端的输出流
OutputStream out = socket.getOutputStream();
// 上传成功后向客户端写出“上传成功”
out.write("上传成功".getBytes());
// 关闭输出流对象
fos.close();
// 关闭Socket对象
socket.close();
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
}
服务器端运行结果,运行结果如图所示。
上述代码中,第10行代码用于创建一个ServerSocket对象,第11~16
行代码用于在while(true)无限循环中调用ServerSocket的accept()方法来接收客户端的请求,没当和一个客户端建立Socket连接后,就开启一个新的线程和这个客户端进行交互,开启的新线程是通过实现Runnable接口创建的,重写的run()方法中实现了服务端接收并保存客户端上传文件的功能在第34行代码上对文件的保存目录用一个File对象进行封装,如果这个目录不存在就调用File的mkdir()方法创建这个目录,为了避免存放的图片名重复而导致的新上传的文件将已经存在的文件覆盖,在第30行代码定义了一个整型变量count,用于统计文件的数目,使用“IP地址(count).jpg”作为上传文件的名称。
在第42~46
行代码用于对表示文件名的File对象进行循环判断,如果文件名存在则一直执行count++。最后将从客户端接收的文件信息写入到指定的目录中,在第58~60行代码用于获取服务端的输出流,向客户端输出“上传成功”信息。通过图中运行结果可以看出,服务器端进入阻塞状态,等待客户端连接。
(2)创建客户端用于上传文件,其代码如下所示。
FileClient.java
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
public class FileClient {
public static void main(String[] args) throws Exception {
// 创建客户端Socket
Socket socket = new Socket("127.0.0.1", 10001);
// 获取Socket的输出流对象
OutputStream out = socket.getOutputStream();
// 创建FileInputStream对象
System.out.println("请输入你要上传文件的路径:");
Scanner sc =new Scanner(System.in);
String upload = sc.nextLine();
if(!upload.isEmpty()){
FileInputStream fis = new FileInputStream(upload);
// 定义一个字节数组
byte[] buf = new byte[1024];
// 定义一个int类型的变量len
int len;
// 循环读取数据
while ((len = fis.read(buf)) != -1) {
out.write(buf, 0, len);
}
// 关闭客户端输出流
socket.shutdownOutput();
// 获取Socket的输入流对象
InputStream in = socket.getInputStream();
// 定义一个字节数组
byte[] bufMsg = new byte[1024];
// 接收服务端的信息
int num = in.read(bufMsg);
String Msg = new String(bufMsg, 0, num);
System.out.println(Msg);
// 关键输入流对象
fis.close();
// 关闭Socket对象
socket.close();
}else {
System.out.println("对不起请您输入文件路径后再上传!!!");
}
}
}
客户端运行结果如图所示。
上述代码中,第9行代码用于创建一个Socket对象,指定连接服务器的IP地址和端口号,然后获取Socket的输出流对象。第17~25行代码用于创建FileInputStream对象读取我们键盘录入的文件名称,并通过Socket的输出流对象向服务端发送文件。
发送完毕后调用Socket的shutDownOutput()方法关闭客户端的输出流。需要注意的是shutDownOutput()方法非常重要,因为服务器端程序在while循环中读取客户端发送的数据时,如果读取不到数据,fis.read(buf)方法会返回-1。也就是说,只要返回的值不是-1。就说明还有数据,需要一直读取,只有fis.read(buf)方法返回的值是-1时循环才会结束。
如果客户端不调用shutDownOutput()方法关闭输出流,服务端的fis.read(buf)方法就不会返回-1,而会一直执行while循环,同时客户端读取服务端数据的read(byte [])方法也是一个阻塞方法,这样服务端和客户端程序就进入了一个“死锁”状态。两个程序都不能结束。客户端上传图片成功后,会读取服务端发送的“上传成功”信息,至此,客户端的程序编写完成。