目录
Address already in use: bind 经典错误
一、网络通信
二、网络通信基本模式
-
C/S模式
-
B/S模式
客户端在网页实现,Web开发使用B/S模式
客户端又称用户端是给用户使用的,服务端是为客户端提供服务,相当于后台
三、网络通信三要素
1、IP地址
本计算机要访问网站并不会使用ip地址,而是使用它的域名,计算机会将这个域名发送到自带的DNS域名解析服务器中,返回存储的域名对应的ip地址,如果本计算机中没有记录,他就会通过网络访问运营商的DNS域名解析服务器来获取ip地址,再返回给本计算机,本计算机通过这个ip地址访问网站服务器,然后返回信息到本计算机。
ip地址与域名是一对多的关系。一个ip地址可以对应多个域名,但是一个域名只有一个ip地址。ip地址是数字组成的,不方便记忆,所以有了域名,通过域名地址就能找到ip地址。
2、IP地址操作类-InetAddress
public static void main(String[] args) throws Exception {
//1、获取本机地址对象
InetAddress ip1 = InetAddress.getLocalHost();
System.out.println(ip1);
//获取本机名
System.out.println(ip1.getHostName());
//获取本机ip
System.out.println(ip1.getHostAddress());
//2、获取域名的ip对象
InetAddress ip2 = InetAddress.getByName("www.baidu.com");
System.out.println(ip2.getHostName());
System.out.println(ip2.getHostAddress());
//3、获取公网IP对象
InetAddress ip3 = InetAddress.getByName("36.152.44.96");
System.out.println(ip3.getHostAddress());
System.out.println(ip3.getHostName());
//4、判断能否ping互通 多少毫秒内能否互通
System.out.println(ip1.isReachable(5000));
}
3、端口号
标记主机中运行的进程(程序)
4、协议
三次握手原理
第一次握手相当于向服务器确认客户端能否发送信息给服务端,第二次握手相当于向客户端确认服务端能否接收和回应消息,第三次握手相当于向服务端确认客户端能否接受和回应消息。
四次挥手原理
四、UDP通信
1、UDP快速入门
模型概念:UDP相当于两个人以丢掷的方式传菜,发送端看准接收端将菜丢过去,然后让接收端去接菜,中间的风险因素不可控,菜(数据)可能会丢失,所以不安全。
也可以指定发送端的端口号
2、UDP通信的实现 (一发一收)
public class ClientDemo1 {
//发送端 客户端
public static void main(String[] args) throws Exception {
System.out.println("============客户端启动==========");
//1、创建发送端对象 (人)
DatagramSocket socket = new DatagramSocket();
//2、创建一个数据包对象封装数据 (韭菜盘子)
/**
* public DatagramPacket(byte buf[], int length,
* InetAddress address, int port) {
* this(buf, 0, length, address, port);
* }
* 参数一:封装要发送的数据 (韭菜)
* 参数二:发送数据的大小
* 参数三:接收端的IP地址
* 参数四:接收端的端口号
*/
byte[] buffer = "我是一个卷心菜".getBytes();
//服务端也在本机,所以填本机的端口号,不在本机则InetAddress.getByName()
DatagramPacket packet = new DatagramPacket(buffer,buffer.length, InetAddress.getLocalHost(),8888);
//3、发送数据
socket.send(packet);
//4、关闭管道对象
socket.close();
}
}
//一发一收
public class ServerDemo2 {
//接收端 服务端
public static void main(String[] args) throws Exception {
//先启动服务端再启动客户端
System.out.println("============服务端启动==========");
//1、创建接收端对象 (接菜的人)
DatagramSocket socket = new DatagramSocket(8888);
//2、创建一个数据包接收对象 (韭菜盘子)
//每个数据包大小限制在64kB以内
byte[] bytes = new byte[1024 * 64];
DatagramPacket packet = new DatagramPacket(bytes,bytes.length);
//3、等待接收数据
socket.receive(packet);
//4、取出数据
//读多少取多少
int len = packet.getLength();
String s = new String(bytes,0,len);
System.out.println(s);
//5、获取发送端的ip地址
String ip = packet.getSocketAddress().toString();
System.out.println("对方地址" + ip);
int port = packet.getPort();
System.out.println("对方端口号" + port);
//6、关闭管道
socket.close();
}
}
3、UDP实现多发多收
//多发多收
public class ClientDemo2 {
//发送端 客户端
public static void main(String[] args) throws Exception {
System.out.println("============客户端启动==========");
//1、创建发送端对象 (人)
//可以指定端口号
DatagramSocket socket = new DatagramSocket(6666);
//扫描器对象
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("请输入:");
//用户输入弹幕
String s = sc.nextLine();
if ("N".equals(s)){
System.out.println("离线成功!");
socket.close();
break;
}
//封装用户数据
byte[] buffer = s.getBytes();
//2、创建一个数据包对象封装数据 (韭菜盘子)
//服务端也在本机,所以填本机的端口号,不在本机则InetAddress.getByName()
DatagramPacket packet = new DatagramPacket(buffer,buffer.length, InetAddress.getLocalHost(),8888);
//3、发送数据
socket.send(packet);
}
}
}
public class ServerDemo2 {
//接收端 服务端
public static void main(String[] args) throws Exception {
//先启动服务端再启动客户端
System.out.println("============服务端启动==========");
//1、创建接收端对象 (接菜的人)
DatagramSocket socket = new DatagramSocket(8888);
//2、创建一个数据包接收对象 (韭菜盘子)
//每个数据包大小限制在64kB以内
byte[] bytes = new byte[1024 * 64];
DatagramPacket packet = new DatagramPacket(bytes,bytes.length);
while (true) {
//3、等待接收数据
socket.receive(packet);
//4、取出数据
//读多少取多少
int len = packet.getLength();
String s = new String(bytes,0,len);
System.out.println(s);
}
}
}
Address already in use: bind 经典错误
一个端口只能被一个程序使用,这里绑定了两个8888的端口,会报错
4、广播和组播
//广播的实现
//创建一个数据包对象封装数据 (韭菜盘子)
//服务端也在本机,所以填本机的端口号,不在本机则InetAddress.getByName()
//使用广播地址255.255.255.255,通过广播地址通信
//局域网内声明同样的端口就能接收到消息
DatagramPacket packet = new DatagramPacket(buffer,buffer.length,
InetAddress.getByName("255.255.255.255"),8888);
//3、发送数据
socket.send(packet);
System.out.println("============服务端启动==========");
//1、创建接收端对象 (接菜的人)
MulticastSocket socket = new MulticastSocket(8888);
//绑定客户端组播地址
//socket.joinGroup(InetAddress.getByName("224.1.1.1"));
//参数一:当前组播地址
//参数二:当前主机所在网段
socket.joinGroup(new InetSocketAddress(InetAddress.getByName("224.1.1.1"),8888),
NetworkInterface.getByInetAddress(InetAddress.getLocalHost()));
//客户端
//封装用户数据
byte[] buffer = s.getBytes();
//2、创建一个数据包对象封装数据 (韭菜盘子)
//服务端也在本机,所以填本机的端口号,不在本机则InetAddress.getByName()
//组播地址224.0.0.0 ~ 239.255.255.255,区间内的地址都可以作为组播地址
DatagramPacket packet = new DatagramPacket(buffer,buffer.length,
InetAddress.getByName("224.1.1.1"),8888);
五、TCP通信
管道通信。在java中只要是使用java.net.Socket类实现通信,底层都是使用TCP协议
接收端
public class ClientDemo1 {
//Socket网络编程 客户端开发
public static void main(String[] args) throws Exception {
//1、创建Socket通信管道请求服务端的连接
Socket socket = new Socket(InetAddress.getLocalHost(),9999);
//2、从Socket通信管道中得到一个字节输出流,负责发送数据
OutputStream os = socket.getOutputStream();
//将低级的字节输出流包装成打印流
PrintStream ps = new PrintStream(os);
//3、发送数据
ps.print("我是一个卷心菜");
//4、刷新管道
ps.flush();
//5、关闭管道
//一般不建议关闭这个管道,因为管道一旦连接代表建立了某种联系
//如果这边关闭管道,可能导致发送的数据丢失,因为关闭管道发送的资源很小,可能会立马被接收端接收
//如果一端管道关闭,另一端也会立马关闭,导致中间的数据丢失
}
}
public class ServerDemo1 {
//Socket网络编程 服务端
public static void main(String[] args) {
try {
//1、注册端口
ServerSocket serverSocket = new ServerSocket(9989);
//2、必须要调用accept方法,等待接收客户端的Socket连接请求,建立Socket通信
//只接收了一个客户端连接,目前服务端是单线程,每次只能处理一个客户端的消息
Socket socket = serverSocket.accept();
//3、从Socket通信管道中获取一个字节输入流
InputStream is = socket.getInputStream();
//将字节输入流转换成字符输入流
InputStreamReader isr = new InputStreamReader(is);
//将低级的字符输入流包装成高级的字符输出流
BufferedReader br = new BufferedReader(isr);
//4、接收数据,一次接收一行
String s;
//如果想接收一行数据,发送端必须要发出一行数据,如果发送端不换行发送则这里会一直等待接收完,会报错
if ((s = br.readLine()) != null) {
System.out.println(socket.getRemoteSocketAddress() + "发送了:" + s);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
1、TCP实现多发多收
//服务端
//如果想接收一行数据,发送端必须要发出一行数据,如果发送端不换行发送则这里会一直等待接收完,会报错
while ((s = br.readLine()) != null) {
System.out.println(socket.getRemoteSocketAddress() + "发送了:" + s);
}
//客户端
while (true) {
//3、发送数据
System.out.print("请说:");
String s = sc.nextLine();
ps.println(s);
//4、刷新管道
ps.flush();
}
2、TCP实现多发多收,同时接收多个客户端的消息
服务端只有一个主线程,内部使用一个while死循环来接收Socket连接,因为要接收多个客户端的消息,使用死循环不断接收其他客户端消息,每发来一次客户端请求,就会产生一个socket管道,死循环执行第一轮,将它交给一个独立的子线程进行处理。
就是一个程序可以被多次同时启动
public class ServerDemo1 {
//Socket网络编程 服务端
public static void main(String[] args) {
try {
//1、注册端口
ServerSocket serverSocket = new ServerSocket(9989);
/*
定义一个死循环,由主线程负责不断的接收客户端的Socket管道连接
*/
while (true) {
//2、必须要调用accept方法,等待接收客户端的Socket连接请求,建立Socket通信
Socket socket = serverSocket.accept();
//上线逻辑
System.out.println(socket.getRemoteSocketAddress() + "上线了!!");
//每接收到一个客户端的Socket管道,就交给一个独立的线程处理
//开始创建独立线程处理socket
//来一个管道就交给一个线程
new ServerThread(socket).start();
//处理接收数据写在线程的run方法中
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
//创建线程类,重写run方法处理接受的客户端信息
public class ServerThread extends Thread{
//线程类作为子线程,要接收客户端的管道
private Socket socket;
public ServerThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
//3、从Socket通信管道中获取一个字节输入流
InputStream is = socket.getInputStream();
//将字节输入流转换成字符输入流
InputStreamReader isr = new InputStreamReader(is);
//将低级的字符输入流包装成高级的字符输出流
BufferedReader br = new BufferedReader(isr);
//4、接收数据,一次接收一行
String s;
//如果想接收一行数据,发送端必须要发出一行数据,如果发送端不换行发送则这里会一直等待接收完,会报错
while ((s = br.readLine()) != null) {
System.out.println(socket.getRemoteSocketAddress() + "发送了:" + s);
}
} catch (Exception e) {
//如果出异常说明客户端挂了,下线逻辑
System.out.println(socket.getRemoteSocketAddress() + "下线了!!");
}
}
}
3、使用线程池优化处理多个客户端
//多发多收,处理多个客户端
public class ServerDemo1 {
//Socket网络编程 服务端
//使用静态变量记住一个线程池不允许修改
private static ExecutorService pool = new ThreadPoolExecutor(2,4,2, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
public static void main(String[] args) {
try {
//1、注册端口
ServerSocket serverSocket = new ServerSocket(9989);
/*
定义一个死循环,由主线程负责不断的接收客户端的Socket管道连接
*/
while (true) {
//2、必须要调用accept方法,等待接收客户端的Socket连接请求,建立Socket通信
Socket socket = serverSocket.accept();
//上线逻辑
System.out.println(socket.getRemoteSocketAddress() + "上线了!!");
//创建任务对象
Runnable runnable = new MyRunnable(socket);
//将任务对象交给线程池处理
pool.execute(runnable);
//处理接收数据写在线程的run方法中
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
//创建Runnable任务对象
public class MyRunnable implements Runnable{
//让每个任务都接收这个端口
private Socket socket;
public MyRunnable(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
//3、从Socket通信管道中获取一个字节输入流
InputStream is = socket.getInputStream();
//将字节输入流转换成字符输入流
InputStreamReader isr = new InputStreamReader(is);
//将低级的字符输入流包装成高级的字符输出流
BufferedReader br = new BufferedReader(isr);
//4、接收数据,一次接收一行
String s;
//如果想接收一行数据,发送端必须要发出一行数据,如果发送端不换行发送则这里会一直等待接收完,会报错
while ((s = br.readLine()) != null) {
System.out.println(socket.getRemoteSocketAddress() + "发送了:" + s);
}
} catch (Exception e) {
//如果出异常说明客户端挂了,下线逻辑
System.out.println(socket.getRemoteSocketAddress() + "下线了!!");
}
}
}
4、即时通信
/**
* 客户端实现收数据和发数据
*/
public class ClientDemo1 {
//Socket网络编程 客户端开发
public static void main(String[] args) throws Exception {
//1、创建Socket通信管道请求服务端的连接
Socket socket = new Socket(InetAddress.getLocalHost(),9989);
//创建一个独立的线程专门负责这个客户端的读消息(服务端随时转发消息过来)
/**
* 读消息线程
*/
new ClientThread(socket).start();
/**
* 主线程发消息
*/
//2、从Socket通信管道中得到一个字节输出流,负责发送数据
OutputStream os = socket.getOutputStream();
//将低级的字节输出流包装成打印流
PrintStream ps = new PrintStream(os);
Scanner sc = new Scanner(System.in);
while (true) {
//3、发送数据
System.out.println("请说:");
String s = sc.nextLine();
ps.println(s);
//4、刷新管道
ps.flush();
}
}
}
//客户端线程
public class ClientThread extends Thread{
//线程类作为子线程,要接收客户端的管道
private Socket socket;
public ClientThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
//3、从Socket通信管道中获取一个字节输入流
InputStream is = socket.getInputStream();
//将字节输入流转换成字符输入流
InputStreamReader isr = new InputStreamReader(is);
//将低级的字符输入流包装成高级的字符输出流
BufferedReader br = new BufferedReader(isr);
//4、接收数据,一次接收一行
String s;
//如果想接收一行数据,发送端必须要发出一行数据,如果发送端不换行发送则这里会一直等待接收完,会报错
while ((s = br.readLine()) != null) {
System.out.println(new Date() + "收到消息:" + s);
}
} catch (Exception e) {
//如果出异常说明客户端将此管道挂了
System.out.println("服务端把你踢出去了");
}
}
}
public class ServerDemo1 {
//Socket网络编程 服务端
//定义一个静态的List集合存储当前在线的所有soket管道
public static List<Socket> allSocketLists = new ArrayList<>();
public static void main(String[] args) {
try {
//1、注册端口
ServerSocket serverSocket = new ServerSocket(9989);
/*
定义一个死循环,由主线程负责不断的接收客户端的Socket管道连接
*/
while (true) {
//2、必须要调用accept方法,等待接收客户端的Socket连接请求,建立Socket通信
Socket socket = serverSocket.accept();
//上线逻辑
System.out.println(socket.getRemoteSocketAddress() + "上线了!!");
//将上线的socket加进集合中
allSocketLists.add(socket);
//将任务对象交给线程处理
new ServerThread(socket).start();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
//服务端线程
public class ServerThread extends Thread{
//线程类作为子线程,要接收客户端的管道
private Socket socket;
public ServerThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
//3、从Socket通信管道中获取一个字节输入流
InputStream is = socket.getInputStream();
//将字节输入流转换成字符输入流
InputStreamReader isr = new InputStreamReader(is);
//将低级的字符输入流包装成高级的字符输出流
BufferedReader br = new BufferedReader(isr);
//4、接收数据,一次接收一行
String s;
//如果想接收一行数据,发送端必须要发出一行数据,如果发送端不换行发送则这里会一直等待接收完,会报错
while ((s = br.readLine()) != null) {
System.out.println(socket.getRemoteSocketAddress() + "发送消息:" + s + new Date());
//将客户端发送的数据进行端口转发 给所有端口
//群发消息
sendMsgToAllSocket(s);
}
} catch (Exception e) {
//如果出异常说明客户端将此管道挂了
System.out.println(socket.getRemoteSocketAddress() + "下线了");
//下线了端口关闭,从列表中清除
ServerDemo1.allSocketLists.remove(socket);
}
}
/**
* 将客户端发送的数据进行端口转发 给所有端口
* @param s
* @throws Exception
*/
private void sendMsgToAllSocket(String s) throws Exception {
for (Socket socket : ServerDemo1.allSocketLists) {
PrintStream ps = new PrintStream(socket.getOutputStream());
ps.println(s);
ps.flush();
}
}
}
5、B/S模式设计
public class BrownServer {
//使用静态变量记住一个线程池不允许修改
private static ExecutorService pool = new ThreadPoolExecutor(2,4,2, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
public static void main(String[] args) {
try {
//1、注册端口
ServerSocket serverSocket = new ServerSocket(8080);
/*
定义一个死循环,由主线程负责不断的接收客户端的Socket管道连接
*/
while (true) {
//2、必须要调用accept方法,等待接收客户端的Socket连接请求,建立Socket通信
Socket socket = serverSocket.accept();
//上线逻辑
System.out.println(socket.getRemoteSocketAddress() + "上线了!!");
//创建任务对象
Runnable runnable = new MyRunnable(socket);
//将任务对象交给线程池处理
pool.execute(runnable);
//处理接收数据写在线程的run方法中
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
//Runnable任务类
public class MyRunnable implements Runnable{
//让每个任务都接收这个端口
private Socket socket;
public MyRunnable(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
//浏览器 已经与本线程建立了Socket管道
//响应消息给浏览器显示
PrintStream ps = new PrintStream(socket.getOutputStream());
//必须响应HTTP协议格式,否则浏览器不认识消息
ps.println("Http/1.1 200 OK"); //协议类型和版本,响应成功的消息
ps.println("Content-Type:text/html;charset=UTF-8"); //响应的数据类型:文本/网页
//必须发送一个空行开始输入文本内容
ps.println();
ps.println("<span style='color:red;font-size:90px'> 《HelloWorld》 </span> ");
ps.close();
} catch (Exception e) {
//如果出异常说明客户端挂了,下线逻辑
System.out.println(socket.getRemoteSocketAddress() + "下线了!!");
}
}
}