【自用】JavaSE--网络通信
概述
一般专业的软件就是使用CS架构,而像是京东这种网站则是采用BS架构,在企业中更多的是BS
无论是CS架构还是BS架构都必须依赖网络编程
网络通信三要素
发送流程:
IP
如果本地dns也找不到,就会去运营商那里找(本地dns肯定知道运营商的地址,具体可复习计算机网络应用层)
InetAddress类
示例:
端口
周知端口 --> 熟知端口:分配给最重要的一些应用程序,让所有人知道
注册端口 --> 登记端口: 分配给稍微不那么重要的,必须在IANA登记,避免重复
动态端口 --> 短暂端口:仅在进程启动时使用,进程通信结束后会被回收,以供其他进程使用
协议
三报文握手、四报文挥手细节可以复习计算机网络传输层
UDP通信
常用方法:
建立客户端与服务端一发一收案例:
public class Client {public static void main(String[] args) {try {//创建客户端对象,即发数据的人DatagramSocket socket = new DatagramSocket();//要发送的数据,使用字节数组封装,使用getBytes()可转换成字节数组byte[] send_data = "你好,我是一头大肥猪".getBytes();//创建数据包DatagramPacket data = new DatagramPacket(send_data, send_data.length, InetAddress.getLocalHost(), 6666);//发送数据包socket.send(data);System.out.println("数据发送完毕");socket.close();//关闭通信,释放资源} catch (Exception e) {e.printStackTrace();}}
}
public class Server {public static void main(String[] args) {try {System.out.println("----服务端已启动----");//创建服务端对象,即接数据的人 要注册端口,用于发送方识别DatagramSocket socket = new DatagramSocket(6666);//接收数据的大小,将能收到的数据的最大值设为64KB(UDP规定一个包最大64KB)byte[] buffer = new byte[1024*64];//创建数据包,用于接数据(理解为收到的packet就装到这个里面)DatagramPacket packet = new DatagramPacket(buffer, buffer.length);//接收数据包,如果没收到,程序会停在这一步直到收到数据包socket.receive(packet);//接收到数据后,buffer里面装的就是接收到的数据了int len = packet.getLength();//获取包的长度String rs = new String(buffer,0,len);System.out.println(rs);//接收到的包中也包含了客户端的IP地址与端口号,可以打印出来System.out.println("接收到的客户端IP地址:"+packet.getAddress().getHostAddress());System.out.println("接收到的客户端 端口:"+packet.getPort());socket.close();//关闭通信,释放资源} catch (Exception e) {e.printStackTrace();}}
}
建立客户端与服务端多发多收案例(循环实现):
首先设置允许单程序多次启动,方法如下图
public class Client {public static void main(String[] args) {try {//创建客户端对象(端口随机分配),即发数据的人DatagramSocket socket = new DatagramSocket();Scanner sc = new Scanner(System.in);while (true) {System.out.println("请输入要发送的信息(输入exit退出):");String msg = sc.next();if(msg.equals("exit")){socket.close();//关闭通信,释放资源System.out.println("已退出");break;}//将输入的数据使用字节数组封装,使用getBytes()可转换成字节数组byte[] send_data = msg.getBytes();//创建数据包DatagramPacket data = new DatagramPacket(send_data, send_data.length, InetAddress.getLocalHost(), 6666);//发送数据包socket.send(data);System.out.println("数据已发送");}} catch (Exception e) {e.printStackTrace();}}
}
public class Server {public static void main(String[] args) {try {System.out.println("----服务端已启动----");//创建服务端对象,即接数据的人 要注册端口,用于发送方识别DatagramSocket socket = new DatagramSocket(6666);while (true) {//接收数据的大小,将能收到的数据的最大值设为64KB(UDP规定一个包最大64KB)byte[] buffer = new byte[1024*64];//创建数据包,用于接数据(理解为收到的packet就装到这个里面)DatagramPacket packet = new DatagramPacket(buffer, buffer.length);//接收数据包,如果没收到,程序会停在这一步直到收到数据包socket.receive(packet);//接收到数据后,buffer里面装的就是接收到的数据了int len = packet.getLength();//获取包的长度String rs = new String(buffer,0,len);System.out.println(rs);//接收到的包中也包含了客户端的IP地址与端口号,可以打印出来System.out.println("接收到的客户端IP地址:"+packet.getAddress().getHostAddress());System.out.println("接收到的客户端 端口:"+packet.getPort());System.out.println("-------------------");//socket.close();//服务端不需要关闭通信}} catch (Exception e) {e.printStackTrace();}}
}
运行示例:
TCP通信
客户端常用方法
服务端常用方法
getRemoteSocketAddress 返回连接的IP地址与端口号
注:双方使用的流一定要一一对应,比如客户端将低级流使用高级流封装,那么服务端必须要使用相同的流封装,且读写方法要一致(用 writeUTF 写,就必须用 readUTF 读)
温故知新 ---> flush: 刷新流,将缓冲区中的数据写到外存中去(关闭流包含刷新操作)
代码示例(多发多收,但暂不支持多个客户端与一个服务端通信):
public class Client {public static void main(String[] args) {try {//创建客户端对象 参数1:指定服务器IP 参数2:服务器中接收数据的端口号Socket socket = new Socket(InetAddress.getLocalHost(),6666);//通过socket方法获得输出流管道,可以通过管道输出数据OutputStream outputStream = socket.getOutputStream();//将低级的输出流管道封装成高级的输出流管道(数据输出流)DataOutputStream dos = new DataOutputStream(outputStream);//调用数据输出流的方法直接写出数据Scanner sc = new Scanner(System.in);while (true) {System.out.println("请输入您要发送的数据(输入exit退出):");String msg = sc.next();if(msg.equals("exit")){System.out.println("通信结束");dos.close();//关闭管道,释放资源socket.close();//关闭通信,释放资源break;}try {//由于是通过管道连接,因此只要有一方断开连接,整个连接就会断开//因此当写异常就代表服务端断开连接了,可以这里捕获异常dos.writeUTF(msg);} catch (IOException e) {System.out.println("服务器已断开连接");break;}dos.flush();}} catch (Exception e) {e.printStackTrace();}}
}
public class Server {public static void main(String[] args) {try {System.out.println("--------服务端已启动---------");//创建服务端对象,将端口设置为6666ServerSocket serverSocket = new ServerSocket(6666);//阻塞等待请求连接的信号,连接成功后,socket会与客户端相通,就可以通过IO流传输数据Socket socket = serverSocket.accept();//获取输入流管道,接收数据InputStream inputStream = socket.getInputStream();//将低级的输出流管道封装成高级的输出流管道(数据输出流)DataInputStream dis = new DataInputStream(inputStream);while (true){try {//由于是通过管道连接,因此只要有一方断开连接,整个连接就会断开//当客户端输出exit后会断开连接,readUTF会异常,在这里捕获即可System.out.println(dis.readUTF());} catch (IOException e) {System.out.println("客户端 "+socket.getRemoteSocketAddress()+" 已断开连接");dis.close();socket.close();//既然客户端那边已经断开了,这边也应该释放掉break;}System.out.println("发送方的IP地址:"+socket.getInetAddress());System.out.println("发送方的端口号:"+socket.getPort());System.out.println("-------------------");}} catch (Exception e) {e.printStackTrace();}}
}
之前实现的TCP通信不支持多客户端的通信,如何实现多客户端与一个服务端通信? -----> 多线程
每当主线程接收到一个与客户端的通信连接之后,就会得到一个socket通信管道,再专门创建出一个子线程来负责这个socket管道的通信
示例:
public class Client {public static void main(String[] args) {try {//创建客户端对象 参数1:指定服务器IP 参数2:服务器中接收数据的端口号Socket socket = new Socket(InetAddress.getLocalHost(),6666);//通过socket方法获得输出流管道,可以通过管道输出数据OutputStream outputStream = socket.getOutputStream();//将低级的输出流管道封装成高级的输出流管道(数据输出流)DataOutputStream dos = new DataOutputStream(outputStream);//调用数据输出流的方法直接写出数据Scanner sc = new Scanner(System.in);while (true) {System.out.println("请输入您要发送的数据(输入exit退出):");String msg = sc.next();if(msg.equals("exit")){System.out.println("通信结束");dos.close();//关闭管道,释放资源socket.close();//关闭通信,释放资源break;}try {//由于是通过管道连接,因此只要有一方断开连接,整个连接就会断开//因此当写异常就代表服务端断开连接了,可以这里捕获异常dos.writeUTF(msg);} catch (IOException e) {System.out.println("服务器已断开连接");System.out.println("-------------------");break;}dos.flush();}} catch (Exception e) {e.printStackTrace();}}
}
public class Server {public static void main(String[] args) {try {System.out.println("--------服务端已启动---------");//创建服务端对象,将端口设置为6666ServerSocket serverSocket = new ServerSocket(6666);while (true) {//阻塞等待请求连接的信号,连接成功后,socket会与客户端相通,就可以通过IO流传输数据Socket socket = serverSocket.accept();//socket管道连通后,交给线程处理new ServerReaderThread(socket).start();}} catch (Exception e) {e.printStackTrace();}}
}
public class ServerReaderThread extends Thread{private Socket socket;public ServerReaderThread(Socket socket) {this.socket = socket;}@Overridepublic void run() {try {//获取输入流管道,接收数据InputStream inputStream = socket.getInputStream();//将低级的输出流管道封装成高级的输出流管道(数据输出流)DataInputStream dis = new DataInputStream(inputStream);System.out.println("客户端 "+socket.getRemoteSocketAddress()+" 已连接");System.out.println("-------------------");while (true){try {//由于是通过管道连接,因此只要有一方断开连接,整个连接就会断开//当客户端输出exit后会断开连接,readUTF会异常,在这里捕获即可System.out.println(socket.getRemoteSocketAddress()+":"+dis.readUTF());} catch (IOException e) {System.out.println("客户端 "+socket.getRemoteSocketAddress()+" 已断开连接");System.out.println("-------------------");dis.close();socket.close();//既然客户端那边已经断开了,这边也应该释放掉break;}System.out.println("-------------------");}} catch (Exception e) {e.printStackTrace();}}
运行结果:
TCP综合案例
群聊
端口转发的思想:服务器收到后,再转发给其他全部客户端
代码示例:
public class Client {public static void main(String[] args) {try {//创建TCP客户端对象Socket socket = new Socket(InetAddress.getLocalHost(),6666);//建立输出流管道并用高级流封装OutputStream os = socket.getOutputStream();DataOutputStream dos = new DataOutputStream(os);Scanner sc = new Scanner(System.in);//子线程负责读数据new ClientReaderThread(socket).start();//主线程正在写数据出去,可以再开一个子线程专门用来收数据while (true){System.out.println("请输入(输入exit退出):");String msg = sc.next();if(msg.equals("exit")){System.out.println("已退出通信");dos.close();socket.close();break;}try {dos.writeUTF(msg);} catch (Exception e) {System.out.println("服务器已断开");break;}dos.flush();//将数据写出去}} catch (Exception e) {e.printStackTrace();}}
}
public class ClientReaderThread extends Thread{private Socket socket;public ClientReaderThread(Socket socket) {this.socket = socket;}@Overridepublic void run() {try {InputStream is = socket.getInputStream();DataInputStream dis = new DataInputStream(is);while(true){try {String msg = dis.readUTF();System.out.println(msg) ;} catch (Exception e) {System.out.println("您已断开连接");dis.close();break;}}} catch (Exception e) {e.printStackTrace();}}
}
public class Server {public static void main(String[] args) {try {System.out.println("-------服务器已启动--------");ServerSocket socket = new ServerSocket(6666);ArrayList<Socket> sockets = new ArrayList<>();//用来记录已连接的客户端while (true) {Socket accept = socket.accept();System.out.println(accept.getRemoteSocketAddress()+" 已建立连接");sockets.add(accept);//接通后就将管道加入集合//交给线程处理new ServerReaderThread(accept,sockets).start();}} catch (Exception e) {e.printStackTrace();}}
}
public class ServerReaderThread extends Thread{private Socket socket;private ArrayList<Socket> sockets;public ServerReaderThread(Socket socket,ArrayList<Socket> sockets) {this.socket = socket;this.sockets = sockets;}@Overridepublic void run() {try {InputStream is = socket.getInputStream();DataInputStream dis = new DataInputStream(is);while (true) {try {String msg = dis.readUTF();System.out.println(socket.getRemoteSocketAddress()+":"+msg);//将收到的信息转发给每个客户端for (Socket s : sockets){ //遍历每一个管道//根据遍历得到的s建立输出流管道OutputStream os = s.getOutputStream();DataOutputStream dos = new DataOutputStream(os);//将msg写出去dos.writeUTF(socket.getRemoteSocketAddress()+":"+msg);dos.flush();}} catch (Exception e) {System.out.println(socket.getRemoteSocketAddress()+" 已断开连接");sockets.remove(socket); //断开连接后集合中也要删除dis.close();socket.close();break;}}} catch (Exception e) {e.printStackTrace();}}
}
运行结果:
实现简易BS架构
浏览器访问服务器的统一标准: http://服务器IP:服务器端口 127.0.0.1表示本地IP地址
8080端口是被用于WWW代理服务的,可以实现网页浏览,经常在访问某个网站或使用代理服务器的时候,会加上“:8080”端口号。
代码示例:
public class Server {public static void main(String[] args) {try {//创建服务器对象,端口设置为8080ServerSocket serverSocket = new ServerSocket(8080);while (true) {//等待接收到响应Socket socket = serverSocket.accept();//连通后响应一下System.out.println(socket.getRemoteSocketAddress()+"已连接");new ServerThread(socket).start();}} catch (IOException e) {e.printStackTrace();}}
}
public class ServerThread extends Thread{private Socket socket;public ServerThread(Socket socket) {this.socket = socket;}@Overridepublic void run() {try {OutputStream os = socket.getOutputStream();//由于是在模拟响应信息给浏览器,因此推荐使用打印流方便格式的输入PrintStream ps = new PrintStream(os);//以下是响应给浏览器必要的数据格式,以后还会深入学ps.println("HTTP/1.1 200 OK");ps.println("Content-Type:text/html;charset=UTF-8");ps.println();//必须换行ps.println("<div style='color:red;font-size:120px;text-align:center'>我是一头大肥猪<div>");socket.close();} catch (Exception e) {e.printStackTrace();}}
}
运行结果:
使用线程池优化
拓展:每次请求都开一个新线程,好不好?
肯定是不好的,如果一亿个人访问,就会创建一亿个线程,直接就卡死了,应当使用线程池
代码示例:
public class Server {public static void main(String[] args) {try {System.out.println("-------服务端已启动-------");//创建服务器对象,端口设置为8080ServerSocket serverSocket = new ServerSocket(8080);//创建线程池ExecutorService pool = new ThreadPoolExecutor(2, 3, 8,TimeUnit.SECONDS, new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());while (true) {//等待接收到响应Socket socket = serverSocket.accept();//连通后响应一下System.out.println(socket.getRemoteSocketAddress()+"已连接");//将管道封装成任务对象Runnable r = new ServerRunnable(socket);pool.execute(r);}} catch (IOException e) {e.printStackTrace();}}
}
public class ServerRunnable implements Runnable{private Socket socket;public ServerRunnable(Socket socket) {this.socket = socket;}@Overridepublic void run() {try {OutputStream os = socket.getOutputStream();//由于是在模拟响应信息给浏览器,因此推荐使用打印流方便格式的输入PrintStream ps = new PrintStream(os);//以下是响应给浏览器必要的数据格式,以后还会深入学ps.println("HTTP/1.1 200 OK");ps.println("Content-Type:text/html;charset=UTF-8");ps.println();//必须换行ps.println("<div style='color:red;font-size:120px;text-align:center'>我是一头大肥猪<div>");socket.close();} catch (Exception e) {e.printStackTrace();}}
}