菜鸟学习笔记:Java提升篇10(网络2——UDP编程、TCPSocket通信)
- UDP编程
- TCP编程(Socket通信)
- 单个客户端的连接
- 多个客户端的连接(聊天室案例)
UDP编程
在上一篇中讲解了UDP协议是非面向连接的不安全但效率较高的通信协议。在了解完概念之后我们用Java来实现UDP编程。
Java中通过DatagramSocket和DatagramPacket来实现UDP通信,通信过程一般分为以下几步:
发送端(客户端):
- 创建客户端DatagramSocket类+端口
- 准备数据
- 打包DatagramPacket+地址及端口
- 发送
- 释放资源
接收端(服务端): - 创建服务端DatagramSocket类+端口
- 准备接收容器
- 接收数据包
- 分析
- 释放资源
依据这个过程我们直接通过代码来理解UDP发送过程:
首先构建服务端:
public class MyServer {
public static void main(String[] args) throws IOException {
//1、创建服务端 +端口
DatagramSocket server = new DatagramSocket(8888);
//2、准备接受容器
byte[] container = new byte[1024];
//3、封装成 包 DatagramPacket(byte[] buf, int length)
DatagramPacket packet =new DatagramPacket(container, container.length) ;
//4、接受数据
server.receive(packet);
//5、分析数据
byte[] data =packet.getData();
int len =packet.getLength();
System.out.println(new String(data,0,len));
//6、释放
server.close();
}
}
这时服务端运行就会进入阻塞状态等待数据:
然后构建客户端
public class MyClient {
public static void main(String[] args) throws IOException {
//1、创建客户端 +端口
DatagramSocket client = new DatagramSocket(6666);
//2、准备数据
String msg ="udp编程";
byte[] data =msg.getBytes();
//3、打包(发送的地点 及端口) DatagramPacket(byte[] buf, int length, InetAddress address, int port)
DatagramPacket packet = new DatagramPacket(data,data.length,new InetSocketAddress("localhost",8888));
//4、发送
client.send(packet);
//5、释放
client.close();
}
}
运行客户端发送数据后服务端就会得到响应:
UDP的特点就是不管有没有服务器等待数据,客户端都可以发送数据,上例中不启动服务客户端运行依然不会报错,但数据会丢失。
TCP编程(Socket通信)
单个客户端的连接
TCP相对UDP而言,需要建立连接、安全可靠但效率较低。它在通信时两端必须连接才能进行通信。
何为Socket,比如用浏览器访问一个网站,点击访问后,浏览器会申请与服务器进行连接,连接后才能进行数据交互操作,在关闭网页后连接会自动断开。我们把客户端与服务端之间建立的连接就称之为Socket。
Java中Socket通信主要使用ServerSocket类来建立连接,通过Socket类来进行数据交互,我们直接通过代码来讲解,注意,交互过程采用的时数据处理流,如果大家忘了可以回去看看,链接:
服务端:
public class Server {
public static void main(String[] args) throws IOException {
//1、创建服务器 指定端口 ServerSocket(int port)
ServerSocket server = new ServerSocket(8888);
//2、接收客户端连接 阻塞式
Socket socket =server.accept();
System.out.println("一个客户端建立连接");
//3、发送数据
String msg ="欢迎使用";
//输出流
/*
BufferedWriter bw = new BufferedWriter(
new OutputStreamWriter(
socket.getOutputStream()));
bw.write(msg);
bw.newLine();
bw.flush();
*/
//socket.getOutputStream()方法可以和之前的FileOutputStream()
//对应起来它表示socket流,下面的write操作会在流中写入数据
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
dos.writeUTF(msg);
dos.flush();
}
}
与UDP协议不同,在连接之前必须先启动服务器,在建立连接后服务端也可以向客户端发送数据。
客户端:
public class Client {
public static void main(String[] args) throws UnknownHostException, IOException {
//1、创建客户端 必须指定服务器+端口 此时就在连接
Socket client = new Socket("localhost",8888);
//2、接收数据
/*
BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream()));
String echo =br.readLine(); //阻塞式方法
System.out.println(echo);
*/
//这里采用之前讲的数据处理流进行交互
DataInputStream dis = new DataInputStream(client.getInputStream());
String echo = dis.readUTF();
System.out.println(echo);
}
}
现在如果直接运行客户端,那么会报如下错误:
需要启动服务器才可以进行数据交互:
多个客户端的连接(聊天室案例)
与多用户交互最简单的方式就是将交互代码写道死循环中,所以服务器端可以改成如下代码:
public static void main(String[] args) throws IOException {
//1、创建服务器 指定端口 ServerSocket(int port)
ServerSocket server = new ServerSocket(8888);
//2、接收客户端连接 阻塞式
while(true){ //死循环 一个accept()一个客户端
Socket socket =server.accept();
System.out.println("一个客户端建立连接");
//3、发送数据
String msg ="欢迎使用";
//输出流
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
dos.writeUTF(msg);
dos.flush();
}
但是这又带来了一个问题,客户端只能一个一个的连接,如果循环体中出现阻塞(比如死循环、等待用户输入等操作),那么后面的客户端就无法建立连接,这里就需要我们之前所学的多线程的知识来解决这个问题。为全面说明这个问题我们采用一个聊天室的案例来讲解。
聊天室案例
首先说明我们要实现的聊天室的功能,多个用户可以通过控制台进行交互,自己输入的内容在其他用户的控制台上会显示。
为实现这个功能,我们需要一个Server端,让其他Client端都与其建立连接,当用户发送消息时,服务器端会把消息转发给其他Client端。依照这个思路我们来完成聊天室。
为便于我们关闭线程,首先创建一个用于关闭线程的工具:
public class CloseUtil {
//Closeable... 表示可以接受不定数量的Closeable类型的参数,它必须数函数接受参数的最后一个
public static void closeAll(Closeable... io){
for(Closeable temp:io){
try {
if (null != temp) {
temp.close();
}
} catch (Exception e) {
// TODO: handle exception
}
}
}
}
针对客户端,我们思考聊天室需要实时的接受和发送数据,这就需要多线程来实现,为此我们创建Send和Receive两个线程类:
Send类
public class Send implements Runnable{
//控制台输入流
private BufferedReader console;
//管道输出流
private DataOutputStream dos;
//控制线程
private boolean isRunning =true;
//无参构造,初始化打印流
public Send() {
console =new BufferedReader(new InputStreamReader(System.in));
}
//有参构造传入与服务器建立的Socket连接
public Send(Socket client){
this();
try {
//初始化输出流
dos =new DataOutputStream(client.getOutputStream());
} catch (IOException e) {
//e.printStackTrace();
isRunning =false;
CloseUtil.closeAll(dos,console);
}
}
//1、从控制台接收数据
private String getMsgFromConsole(){
try {
//接受控制台输入
return console.readLine();
} catch (IOException e) {
//e.printStackTrace();
}
return "";
}
/**
* 1、从控制台接收数据
* 2、发送数据
*/
public void send(){
//接收控制台输入
String msg = getMsgFromConsole();
try {
if(null!=msg&& !msg.equals("")){
//发送输入给服务器
dos.writeUTF(msg);
dos.flush(); //强制刷新
}
} catch (IOException e) {
//e.printStackTrace();
isRunning =false;
CloseUtil.closeAll(dos,console);
}
}
@Override
public void run() {
//线程体
while(isRunning){
send();
}
}
}
Receive类:
public class Receive implements Runnable {
//输入流
private DataInputStream dis ;
//线程标识
private boolean isRunning = true;
public Receive() {
}
public Receive(Socket client){
try {
//初始化输入流
dis = new DataInputStream(client.getInputStream());
} catch (IOException e) {
e.printStackTrace();
isRunning =false;
CloseUtil.closeAll(dis);
}
}
//接收数据
public String receive(){
String msg ="";
try {
msg=dis.readUTF();
} catch (IOException e) {
e.printStackTrace();
isRunning =false;
CloseUtil.closeAll(dis);
}
return msg;
}
@Override
public void run() {
//线程体
while(isRunning){
System.out.println(receive());
}
}
}
在Client中我们启动这两个线程:
public class Client {
public static void main(String[] args) throws UnknownHostException, IOException {
Socket client = new Socket("localhost",9999);
new Thread(new Send(client)).start(); //一条路径
new Thread(new Receive(client)).start(); //一条路径
}
}
由于存在多个用户,所以建立连接过程(server.accept())和用户发送消息过程必须并行处理,所以还需要一个Channel线程来为各个Client端发送数据,为了方便访问Server类中的private属性,我们采用内部类的方式对Channel进行管理,并且还需要建立一个容器来存放已有用户。综上Server端的代码如下:
public class Server {
private List<MyChannel> all = new ArrayList<MyChannel>();
/**
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
new Server().start();
}
public void start() throws IOException{
ServerSocket server =new ServerSocket(9999);
while(true){
Socket client =server.accept();
MyChannel channel = new MyChannel(client);
all.add(channel);//统一管理
new Thread(channel).start(); //一条道路
}
}
/**
* 一个客户端 一条道路
* 1、输入流
* 2、输出流
* 3、接收数据
* 4、发送数据
* @author Administrator
*
*/
private class MyChannel implements Runnable{
private DataInputStream dis ;
private DataOutputStream dos ;
private boolean isRunning =true;
public MyChannel(Socket client ) {
try {
dis = new DataInputStream(client.getInputStream());
dos = new DataOutputStream(client.getOutputStream());
} catch (IOException e) {
//e.printStackTrace();
CloseUtil.closeAll(dis,dos);
isRunning =false;
}
}
/**
* 读取数据
* @return
*/
private String receive(){
String msg ="";
try {
msg=dis.readUTF();
} catch (IOException e) {
//e.printStackTrace();
CloseUtil.closeAll(dis);
isRunning =false;
all.remove(this); //移除自身
}
return msg;
}
/**
* 发送数据
*/
private void send(String msg){
if(null==msg ||msg.equals("")){
return ;
}
try {
dos.writeUTF(msg);
dos.flush();
} catch (IOException e) {
//e.printStackTrace();
CloseUtil.closeAll(dos);
isRunning =false;
all.remove(this); //移除自身
}
}
/**
* 发送给其他客户端
*/
private void sendOthers(){
String msg = this.receive();
//遍历容器
for(MyChannel other:all){
if(other ==this){
continue;
}
//发送其他客户端
other.send(msg);
}
}
@Override
public void run() {
while(isRunning){
sendOthers();
}
}
}
}
这样就完成了聊天室的功能,我们创建两个客户Tom和Bob(可以有多个),代码测试结果如下:
以上就是Java网络的所有内容,这也是第一次展示综合案例,可能代码一多大家对逻辑理解起来会困难一些,建议大家多看几遍,结合聊天室也对之前的IO流、线程、容器好好的做做复习。相信大家要是能搞明白这段代码Java基础的内容差不多就没什么问题了。