网络编程
一、概念
什么是网络编程?
- 网络编程可以让程序和网络上其他设备中的程序进行数据交互。
网络编程的模式有两种:
- C/S架构(Client-Server 客户端/服务器)
- 需要程序员开发实现客户端和服务端,然后用户安装客户端去访问服务端
- B/S架构(Browser/Server 浏览器/服务器)
- 需要程序员开发服务端,然后用户安装浏览器去访问服务端
二、网络通信三要素
实现网络通信的三要素:
- IP地址:设备在网络中的地址,唯一标识
- 端口:应用程序在设备中的唯一标识
- 协议:数据在网络中传输的规则,常见协议有UDP协议和TCP协议
2.1 IP地址
- IP(Internet Protocol):全称“互联网协议地址”,是分配给上网设备的唯一标志。
- 常见IP分类有:IPv4和IPv6
- IPv4采用点分十进制表示法,例如:192.168.1.1
- IPv6采用冒号分十六进制表示法
IP地址形式:
- 公网地址和私有地址(局域网使用)
- 192.168开头是常见的局域网地址,范围为192.168.0.0-192.168.255.255
IP常用命令(cmd下输入):
- ipconfig:查看本机IP地址
- ping IP地址:测试网络连通性
特殊IP地址
- 本机IP:127.0.0.1或者localhost:称为回送地址也可称本地回环地址,只会寻找当前所在本机
2.1.1 IP地址操作类-InetAddress
此类表示Internet协议(IP)地址
2.1.2 常用API
名称 | 说明 |
---|---|
public static InetAddress getLocalHost() | 返回本机地址对象 |
public static InetAddress getByName(String host) | 得到指定IP地址对象,参数是域名或者IP地址 |
public String getHostName() | 返回此IP地址的主机名 |
public String getHostAddress() | 返回IP地址字符串 |
public boolean isReachable(int timeout) | 在指定毫秒内连通该地址对象对应的ip主机,连通返回true |
try {
//获得本机地址对象,并打印该对象的主机名和ip
InetAddress ip1 = InetAddress.getLocalHost();
System.out.println(ip1.getHostName());//LAPTOP-4MRBJ509
System.out.println(ip1.getHostAddress());//192.168.177.1
//得到指定ip或者域名的对象
InetAddress ip2 = InetAddress.getByName("www.baidu.com");
System.out.println(ip2.getHostName());//www.baidu.com
System.out.println(ip2.getHostAddress());//36.152.44.96
//得到指定ip或者域名的对象
InetAddress ip3 = InetAddress.getByName("36.152.44.96");
System.out.println(ip3.getHostName());//36.152.44.96
System.out.println(ip3.getHostAddress());//36.152.44.96
//ping指定对象地址,通了返回true,否则返回false,参数为多少毫秒去连通测试
System.out.println(ip3.isReachable(1000));
} catch (Exception e) {
e.printStackTrace();
}
2.2 端口号
端口号:标识正在计算机设备上运行的进程(程序),被规定为一个16位的二进制,范围为0-65535。
2.2.1 端口类型
- 周知端口:0-1023,被预先定义的知名应用占用(例如:HTTP占80,FTP占21)
- 注册端口:1024-49151,分配给用户进程或某些应用程序。(如:Tomcat占8080,Mysql占3306)
- 动态端口:49152到65535,之所以被称为动态端口,是因为他一般不固定分配某种进程,而是动态分配
注意:我们自己开发的程序选择注册端口,且一个设备中不能出现两个程序的端口号一样,否则会报错冲突
2.3 协议
通信协议
- 连接和通信数据的规则被称为网络通信协议
网络通信两套参考模型
- OSI:世界互联网标准,全球通信规范,由于此模型过于理想化,未能在因特网进行广泛推广
- TCP/IP:事实上的国际标准
传输层常见的协议:
- TCP协议:传输控制协议,面向连接的可靠通信协议
- UDP协议:用户数据报协议,无连接的不可靠通信协议
2.3.1 TCP协议
TCP协议特点:
- 使用TCP协议,必须双方先建立连接,它是一种面向连接的可靠通信协议
- 传输前,采用”三次握手“方式建立连接,所以是可靠的
- 连接中可以进行大数据量的传输
- 连接、发送数据都需要确认,且传输完毕后,还需要释放已建立的连接,通信效率较低。
- 连接结束时进行四次挥手
TCP协议通信场景:
- 对信息安全要求较高的场景。(例如:文件下载、金融等数据通信)
2.3.1.1 三次握手
- 客户端向服务器发出连接请求,等待服务器确认。(客户端发)
- 服务器接收到请求后,向客户端返回一个响应,告诉客户端收到请求,也能够发请求。(服务端收到后,服务端发)
- 客户端收到服务端响应后再次发出确认信息,连接就建立成功。(客户端收到后,客户端发送确认信息)
详解可见:深入浅出TCP三次握手 (多图详解) - 知乎 (zhihu.com)
2.3.1.2 四次挥手
- 客户端向服务器发送取消连接请求
- 服务器向客户端返回一个响应,表示收到了客户端取消请求,并让客户端等会,还有数据没处理完
- 服务器向客户端发送确认消息信息
- 客户端再次发送确认取消,连接取消
2.3.2 UDP协议
- UDP协议是一种无连接、不可靠传输的协议
- 将数据源IP、目的地IP和端口封装成数据包,不需要建立连接
- 每个数据包大小限制在64KB以内
- 发送不管对方是否准备好,接收方收到也不确认,所以是不可靠的
- 可以广播发送,发送数据结束时无需释放资源,开销小,速度快
应用场景:
- 语音通话,视频会议等
三、UDP通信-入门
UDP通信特点:
- UDP是一种无连接、不可靠传输的协议
- 将数据源IP、目的地IP和端口以及数据封装成数据包,大小限制64KB内,直接发送出去即可
UDP发送数据需要有存放数据的容器(数据包对象 DatagramPacket) 和 发送端和接收端的对象(DatagramSocket)
3.1 DatagramPacket
存放数据的容器
构造器 | 说明 |
---|---|
public DatagramPacket(byte[] buf,int length,InetAddress address,int port) | 创建发送端数据包对象 buf:要发送的内容,字节数组 length:要发送内容的字节长度 address:接收端的IP地址对象 port:接收端的端口号 |
public DatagramPacket(byte[],int length) | 创建接收端的数据包对象 buf:用来存储接收的内容 length:能够接收内容的长度 |
方法 | 说明 |
public int getLength() | 获得实际接收到的字节个数 |
3.2 DatagramSocket
发送端和接收端对象
构造器 | 说明 |
---|---|
public DatagramSocket() | 创建发送端Socket对象,系统会随机分配一个端口号 |
public DatagramSocket(int port) | 创建接收端的Socket对象并指定端口号 |
方法 | 说明 |
public void send(DatagramPacket dp) | 发送数据包 |
public void receive(DatagramPacket p) | 接收数据包 |
3.3 练习
3.3.1 一发一收
使用UDP通信实现:接收消息和发送消息
需求:客户端实现步骤
- 创建DatagramSocket对象(发送端的对象)
- 创建DatagramPacket对象封装需要发送的数据
- 使用发送端对象的send方法传入要发送的数据对象
- 释放资源
需求:接收端实现步骤
- 创建接收端对象,并指定端口号
- 创建接收数据
- 使用接收端对象的receiver方法传入接收的数据对象
- 释放资源
public class Client {
public static void main(String[] args) throws Exception {
System.out.println("===客户端启动===");
//创建发送端对象,无需指定端口,计算机会随机分配端口号发送
DatagramSocket socket = new DatagramSocket();
//创建发送端的数据包对象,并指定接收端的端口号
//public DatagramPacket(byte buf[], int offset, int length, InetAddress address, int port)
byte[] str = "你在干嘛呢?".getBytes(StandardCharsets.UTF_8);
//这里我发送给自己,如果想要发送给别人,这里ip地址可以填写对方ip
DatagramPacket dp = new DatagramPacket(str, str.length, InetAddress.getByName("localhost"), 8888);
//发送数据包
socket.send(dp);
//发送后需要关闭资源
socket.close();
}
}
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("===服务端启动===");
//创建接收端对象,需要注册端口,注册端口为发送端指定的端口
DatagramSocket socket = new DatagramSocket(8888);
//创建接收端的数据包,要指定接收的字节数组和数组长度
//public DatagramPacket(byte buf[], int length)
//此处我们将大小设置为64KB,因为UDP最大只能发送64KB内容,如果设置小了就会导致接收信息不全
byte[] str = new byte[1024 * 64];
DatagramPacket dp = new DatagramPacket(str, str.length);
socket.receive(dp);
//接收后,接收的数据存储在字节数组中
// System.out.println(new String(str));//此时因为我们设置的接收大小很大,读取的内容很少,所以会导致盘子大内容少,后面会有很多其他字符
//读多少取多少,我们读的大小取决于发送端数据的大小,所以使用数据包.getLength方法获取发送的数据长度,然后根据长度来读取
System.out.println(new String(str,0,dp.getLength()));
//我们还可以得到发送端的IP地址以及端口号,这些都是存储在数据包中
System.out.println( "对方地址为:"+dp.getSocketAddress().toString() +"对方端口号为:" + dp.getPort());
//对方地址为:/127.0.0.1:61728对方端口号为:61728
//发送完毕关闭资源
socket.close();
}
}
3.3.2 多发多收
在一发一收的基础上增加循环让用户一直输入想要发送的消息,如果想要退出就输入exit退出
场景:例如弹幕
public class Client {
public static void main(String[] args) throws Exception {
Scanner sc = new Scanner(System.in);
System.out.println("===客户端启动===");
//创建发送端对象
DatagramSocket socket = new DatagramSocket();
while(true){
//用户输入
System.out.print("请输入想发送的消息:");
String s = sc.nextLine();
//判断是否退出
if ("exit".equals(s)){
System.out.println("离线成功!!!");
//关闭资源
socket.close();
break;
}
//创建一个字节数组存储要发送的数据
byte[] str = s.getBytes();
DatagramPacket dp = new DatagramPacket(str, str.length, InetAddress.getByName("localhost"), 8888);
//发送
socket.send(dp);
}
}
}
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("===服务端启动===");
//创建接收端
DatagramSocket socket = new DatagramSocket(8888);
//创建数据包
byte[] buff = new byte[1024 * 64];
DatagramPacket dp = new DatagramPacket(buff, buff.length);
//此时服务端可以接收任何地方发来的消息,并展示在服务端,好似看视频时的弹幕
while (true) {
//接收数据
socket.receive(dp);
//将接收的数据变成字符串,读多少打印多少
String str = new String(buff, 0, dp.getLength());
System.out.println("来自ip:" + dp.getSocketAddress() + "端口:" + dp.getPort() + ",说:" + str);
}
}
}
3.4 UDP协议模拟飞秋发消息
try {
DatagramSocket socket = new DatagramSocket();
String str = "我是谁";
int port = 2425;
//发送给谁,ip地址填对方ip
InetAddress address = InetAddress.getByName("192.168.58.1");
//飞秋发消息的协议: 1:100:发送者飞秋名:发送者主机名:32:信息
String msg = "1:100:疯狂的飞秋:发送者主机名:32:" + str;
DatagramPacket p = new DatagramPacket(msg.getBytes(),msg.getBytes().length,address,port);
//UDP协议发送
socket.send(p);
socket.close();
}catch (Exception e){
e.printStackTrace();
}
四、UDP通信-广播、组播
4.1 UDP三种的通信方式
单播:单台主机与单台主机之间的通信
广播:当前主机和所在网络中的所有主机通信
组播:当前主机和选定的一组主机通信
4.2 UDP实现广播
- 使用广播地址:255.255.255.2554
- 操作如下:
- 发送端发送的数据包的目的地写广播地址、且指定端口。(255.255.255.255,9999)
- 本机所在网段的其他主机的程序只要注册对应端口就可以接收到消息了
public class Client {
public static void main(String[] args) throws Exception {
Scanner sc = new Scanner(System.in);
System.out.println("===客户端启动===");
//创建发送端对象
DatagramSocket socket = new DatagramSocket();
//用户输入
System.out.print("请输入想发送的消息:");
String s = sc.nextLine();
//创建一个字节数组存储要发送的数据
byte[] str = s.getBytes();
//指定接收端地址为255.255.255.255,并指定端口为9898
DatagramPacket dp = new DatagramPacket(str, str.length, InetAddress.getByName("255.255.255.255"), 9898);
//发送
socket.send(dp);
}
}
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("===服务端启动===");
//创建接收端(注册发送端指定广播端口9898)
//局域网中只需要所有主机打开9898端口就能接收广播地址
DatagramSocket socket = new DatagramSocket(9898);
//创建数据包
byte[] buff = new byte[1024 * 64];
DatagramPacket dp = new DatagramPacket(buff, buff.length);
//接收数据
socket.receive(dp);
//将接收的数据变成字符串,读多少打印多少
String str = new String(buff, 0, dp.getLength());
System.out.println("来自ip:" + dp.getSocketAddress() + "端口:" + dp.getPort() + ",说:" + str);
}
}
4.3 UDP实现组播
- 使用组播地址:224.0.0.0-239.255.255.255
- 操作如下:
- 发送端的数据包的目的地是组播IP(例如:224.0.0.1 端口:9999)
- 接收端必须绑定该组播(224.0.0.1),端口还要注册发送端的目的端口9999,这样即可接收该组播消息
- DatagramSocket的子类MulticastSocket可以在接收端绑定组播IP
//发送端
DatagramPacket dp = new DatagramPacket(str, str.length, InetAddress.getByName("224.0.0.1"), 9999);
//接收端
MulticastSocket socket = new MulticastSocket(9999);
socket.joinGroup(InetAddress.getByName("224.0.0.1"));
五、TCP通信-入门
TCP协议特点:
- TCP是一种面向连接、安全、可靠的传输数据协议
- 传输前采用“三次握手”方式,点对点通信,是可靠的
- 在连接中可进行大量数据传输
注意:java中只要使用Socket类实现通信,底层即是使用TCP协议
TCP通信原理:
- 客户端怎么发,服务端就应该怎么收消息
- 客户端如果没有消息,服务端会进入阻塞等待
- Socket一方关闭或者出现异常,对方Socket也会失效或者出现异常
5.1 Socket(客户端)
客户端步骤:
- 创建客户端Socket对象,请求与服务端的连接
- 使用socket对象调用getOutputStream()方法得到字节输出流
- 使用字节输出流完成数据的发送
- 释放资源:关闭socket管道
构造器 | 说明 |
---|---|
public Socket(String host,int port) | 创建发送端的Socket对象和服务端连接,参数为服务端程序的ip和端口 |
方法 | 说明 |
OutputStream getOutputStream() | 获得字节输入流对象 |
InputStream getInputStream() | 获得字节输入流 |
5.2 ServerSocket(服务端)
服务端步骤:
- 创建ServerSocket对象,注册服务端端口
- 调用ServerSocket对象的accept()方法,等待客户端连接,并得到Socket管道对象
- 通过Socket对象调用getInputStream()方法得到字节输入流,完成数据的接收
- 释放资源:关闭socket管道
构造器 | 说明 |
---|---|
public ServerSocket(int port) | 注册服务端端口 |
方法 | 说明 |
public Socket accept() | 等待接收客户端的Socket通信连接 连接成功返回Socket对象与客户端建立端到端通信 |
5.3 一发一收
一发一收中客户端按行发消息,服务端就要按行接收消息
public class Client {
public static void main(String[] args){
System.out.println("===客户端启动===");
try {
// 创建Socket管道请求服务器,第一个参数是服务器的ip地址,第二个参数是服务器端口
Socket socket = new Socket(InetAddress.getLocalHost(),9999);
// 从socket中得到一个字节输出流管道,用来发送数据到服务端
OutputStream os = socket.getOutputStream();
// 将输出流包装成打印流
PrintStream ps = new PrintStream(os);
//发送消息,然后刷新
ps.println("我是第一条消息");
ps.flush();
//可以不关闭,防止消息还没发送完直接关闭导致消息不准确
// socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class Server {
public static void main(String[] args){
System.out.println("===服务端启动===");
try {
//注册端口
ServerSocket serverSocket = new ServerSocket(9999);
//必须调用accept方法,等待接收客户端的连接,获得Socket管道
Socket socket = serverSocket.accept();
//获得字节输入流,接收消息
InputStream inputStream = socket.getInputStream();
//将字节输入流转换为字符流并包装成缓冲字符输入流
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
//每次读取一行
String line = "";
while ((line = br.readLine()) != null) {
System.out.println( socket.getRemoteSocketAddress() +"说:" + line);
}
} catch (Exception e) {
System.out.println("离线了");
}
}
}
5.4 多发多收
在一发一收的基础上使用while循环输入消息,服务端使用while循环接收消息
问题:实现了多发多收,那么是否可以同时接收多个客户端消息?
- 不可以,因为服务端现在只有一个线程,一个线程只能与一个客户端进行通信
public class Client {
public static void main(String[] args){
System.out.println("===客户端启动===");
try {
// 创建Socket管道请求服务器,第一个参数是服务器的ip地址,第二个参数是服务器端口
Socket socket = new Socket(InetAddress.getLocalHost(),9999);
// 从socket中得到一个字节输出流管道,用来发送数据到服务端
OutputStream os = socket.getOutputStream();
// 将输出流包装成打印流
PrintStream ps = new PrintStream(os);
//使用while多发多收
while (true) {
Scanner sc = new Scanner(System.in);
System.out.println("请输入你想发送的消息:");
String str = sc.nextLine();
if ("exit".equals(str)){
System.out.println("退出");
break;
}
//发送消息,然后刷新
ps.println(str);
ps.flush();
}
//可以不关闭,防止消息还没发送完直接关闭导致消息不准确
// socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class Server {
public static void main(String[] args){
System.out.println("===服务端启动===");
try {
//注册端口
ServerSocket serverSocket = new ServerSocket(9999);
//必须调用accept方法,等待接收客户端的连接,获得Socket管道
Socket socket = serverSocket.accept();
//获得字节输入流,接收消息
InputStream inputStream = socket.getInputStream();
//将字节输入流转换为字符流并包装成缓冲字符输入流
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
while (true) {
//每次读取一行
String line = "";
while ((line = br.readLine()) != null) {
System.out.println( socket.getRemoteSocketAddress() +"说:" + line);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
六、TCP通信-同时接收多个客户端消息(重点)
- 当我们引入多线程后,就能让服务端同时处理多个客户端通信请求
6.1 引入多线程优化
实现步骤:
- 主线程定义一个循环负责接收客户端Socket管道连接
- 每接收到一个Socket通信管道后,分配一个独立的线程负责处理它
//客户端代码不变
//服务端
public class Server {
public static void main(String[] args){
System.out.println("===服务端启动===");
try {
//注册端口
ServerSocket serverSocket = new ServerSocket(9999);
//使用死循环一直接收新来的客户端,并且分配子线程给他
while (true) {
//必须调用accept方法,等待接收客户端的连接,获得Socket管道
Socket socket = serverSocket.accept();
System.out.println(socket.getRemoteSocketAddress() + "上线了");
//new出新的线程去处理
new ServerThread(socket).start();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
//子线程类
class ServerThread extends Thread {
//创建私有的socket属性
private Socket socket;
//有参构造,传入当前连接客户端Socket管道
public ServerThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
//获得字节输入流,接收消息
InputStream inputStream = socket.getInputStream();
//将字节输入流转换为字符流并包装成缓冲字符输入流
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
//每次读取一行
String line = "";
while ((line = br.readLine()) != null) {
System.out.println( socket.getRemoteSocketAddress() +"说:" + line);
}
} catch (Exception e) {
//当客户端退出后会报错,所以这里我们可以打印该客户端离线,防止报错(虽然报错也不会影响其他线程运行)
System.out.println(socket.getRemoteSocketAddress() + "离线了");
// e.printStackTrace();
}
}
}
6.2 引入线程池优化
为什么要引入线程池?
- 客户端和服务端的线程模型是:N-N的关系
- 客户端并发越多,系统瘫痪越快
- 线程池优化后的弊端:
- 当创建线程池后,当客户端连接后,会有核心线程先处理,如果核心线程都在忙,且任务队列也满了,那么才会启用临时线程处理任务
- 当核心线程、临时线程都在忙,且任务队列满了,就会开始拒绝客户端连接,这个拒绝客户端是看不到的
- 线程池的优势:
- 服务端可以复用线程处理多个客户端,可避免系统瘫痪
- 适合客户端通信时间较短的场景(客户端访问后用一会就离开,就会释放该线程,让该线程去服务其他客户端)
//客户端代码不变
public class Server {
//我们设置静态的线程池
private static ThreadPoolExecutor pool = new ThreadPoolExecutor(2,3,5, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
public static void main(String[] args){
System.out.println("===服务端启动===");
try {
//注册端口
ServerSocket serverSocket = new ServerSocket(9999);
//使用死循环一直接收新来的客户端,并且分配子线程给他
while (true) {
//必须调用accept方法,等待接收客户端的连接,获得Socket管道
Socket socket = serverSocket.accept();
System.out.println(socket.getRemoteSocketAddress() + "上线了");
Runnable serverRunnable = new ServerRunnable(socket);
pool.execute(serverRunnable);
}
} catch (RejectedExecutionException e) {
System.out.println("服务器已爆满");
}catch (Exception e) {
e.printStackTrace();
}
}
}
public class ServerRunnable implements Runnable {
//创建私有的socket属性
private Socket socket;
//有参构造,传入当前连接客户端Socket管道
public ServerRunnable(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
//获得字节输入流,接收消息
InputStream inputStream = socket.getInputStream();
//将字节输入流转换为字符流并包装成缓冲字符输入流
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
//每次读取一行
String line = "";
while ((line = br.readLine()) != null) {
System.out.println( socket.getRemoteSocketAddress() +"说:" + line);
}
} catch (Exception e) {
//当客户端退出后会报错,所以这里我们可以打印该客户端离线,防止报错(虽然报错也不会影响其他线程运行)
System.out.println(socket.getRemoteSocketAddress() + "离线了");
// e.printStackTrace();
}
}
}
七、即时通信
什么是即时通信?
- 即时通信,指一个客户端发消息出去,其他客户端可以接收到。
- 之前我们的所有消息都是发给服务器
- 即时通信需要进行端口转发的设计思想(发送消息给服务端,服务端帮我们转发消息到指定客户端)
public class MyClient {
public static void main(String[] args) {
System.out.println("===客户端启动===");
Scanner sc = new Scanner(System.in);
try {
//连接服务器并创建socket对象,指定服务器的ip和端口号
Socket socket = new Socket(InetAddress.getLocalHost().getHostAddress(),9999);
//因为客户端既要发信息又要收信息,所以将两者区分为两个线程
//子线程用来收消息
new ClientReadThread(socket).start();
//主线程来发消息,这里获得字节输出流我们包装成打印流
PrintStream ps = new PrintStream(socket.getOutputStream());
//因为发消息是可以一直发送的,所以使用循环来发消息,记得刷新
while (true){
System.out.println("说:");
String line = sc.nextLine();
ps.println(line);
ps.flush();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
//客户端收消息
class ClientReadThread extends Thread{
private Socket socket;
public ClientReadThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
//子线程收消息
try {
//获得客户端字节输入流转换成字符流然后包装成缓冲字符输入流(因为缓冲字符输入流可以读行,对方按行发我们需要按行读)
InputStream inputStream = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
//创建字符串用来读转发来的消息
String line = "";
while((line = br.readLine())!= null){
System.out.println(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class MyServer {
//创建一个map集合用来存放连接的客户端
public static Map<String, Socket> map = new HashMap<>();
public static void main(String[] args) {
System.out.println("===服务端启动===");
try {
//注册端口
ServerSocket server = new ServerSocket(9999);
//等待客户端连接(因为客户端不止一台,所以采用多线程,且循环等待)
while (true) {
Socket socket = server.accept();
//截取客户端的ip和端口号用来作为key值存放在集合中
String[] split = socket.getInetAddress().getHostAddress().split("\\.");
String ip = split[split.length - 1];
String user = ip + ":" + socket.getPort();
map.put(user, socket);
System.out.println("欢迎" + user + "用户加入");
System.out.println("当前有:" + MyServer.map.size() + "人");
//给新来的客户端创建一个线程
new MyServerThread(socket, user).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
//为每一个客户端创建一个线程
class MyServerThread extends Thread {
//socket是接收客户端的socket
//user接收该客户端的键值(IP地址+端口号)
private Socket socket;
private String user;
public MyServerThread(Socket socket, String user) {
this.socket = socket;
this.user = user;
}
@Override
public void run() {
try {
//得到客户端输入的内容,并转换和包装成缓冲字符流
InputStream inputStream = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
//按行读
String line = "";
while ((line = br.readLine()) != null) {
//判断是否输入了-,-表示私发,对方名称+‘-’+消息:表示给谁私发什么消息
String[] split = line.split("-");
//判断是私发还是群发
if (split != null && split.length >= 2) {
//私发
//根据指定的名字去map中找Socket对象,然后给他发消息
String str = user + "对你说:" + split[1];
sendTo(MyServer.map.get(split[0]), str);
} else {
//群发
String str = user + "说:" + line;
//转发到所有在线的客户端
sendToAll(str);
}
}
} catch (Exception e) {
MyServer.map.remove(user);
System.out.println(user + "离线了");
System.out.println("还剩:" + MyServer.map.size() + "人");
}
}
//转发消息到所有人
private void sendToAll(String line) {
//获得map的所有值,客户端对象Socket
Collection<Socket> values = MyServer.map.values();
for (Socket value : values) {
//指定消息不发给自己
if (value != socket) {
sendTo(value, line);
} else {
//如果是发给自己,就将名称换成我说
String[] str = line.split(":");
String line2 = "我说:" + str[str.length - 1];
sendTo(value, line2);
}
}
}
//将消息发给指定的人
private void sendTo(Socket socket, String line) {
try {
//获得客户端的字节输出流并包装成打印流
PrintStream ps = new PrintStream(socket.getOutputStream());
//转发消息
ps.println(line);
//刷新
ps.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
}
八、BS系统(了解)
BS结构是什么,需要开发客户端吗?
- 浏览器访问服务端,不需要开发客户端
注意:服务器必须给浏览器响应HTTP协议格式的数据,否则浏览器不识别
TCP通信如何实现BS请求网页信息回来呢?
- 客户端使用浏览器发起请求(不需要开发客户端)
- 服务端必须按照浏览器的协议规则响应数据
- 响应数据格式为HTTP协议格式。