Java网络编程

一、基础知识

IP:

  • 用于标记计算机设备,是分配给上网设备的唯一标志。

  • 有两种形式:IPv4和IPv6。

    IPv4:

    一共32位,分成四段表示。每段用十进制标志。

    IPv6:

    一共128位,分成八段表示,每段每四位编码成一个十六进制位表示,数之间用冒号分开。

image-20250601215225001

示例:

1
2
3
4
5
6
7
8
9
10
//1.获取主机ip
InetAddress localHost = InetAddress.getLocalHost();
System.out.println(localHost.getHostName());
System.out.println(localHost.getHostAddress());
//2.获取域名ip
InetAddress ip2 = InetAddress.getByName("www.zhihu.com");
System.out.println(ip2.getHostName());
System.out.println(ip2.getHostAddress());
//ping www.zhihu.com
System.out.println(ip2.isReachable(6000)); //6000ms内是否能ping通

执行结果:image-20250604001034385

端口:

用于标记计算机设备上运行的应用程序,被规定为一个16位的二进制,范围是0~65535。

协议

1.UDP(用户数据报协议)

特点:无连接、不可靠通信

解释:无连接指的是数据传输之前不事先建立连接。不可靠通信指的是发送端每次把发送的数据(不超过64KB)、接收端IP等信息封装成一个数据包,把这个数据包发出去后就不管了。管它接收端有没有接收到。

java涉及api:

image-20250604001253097

案例

服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class UDPService {
public static void main(String[] args) throws Exception {
//1.创建服务端对象,端口6666
DatagramSocket socket = new DatagramSocket(6666);
System.out.println("服务端启动");
//2.创建数据包对象用于接收数据
byte[] bytes = new byte[1024 * 64]; //设置接收的数据包的大小,固定的
DatagramPacket packet = new DatagramPacket(bytes, bytes.length);

//3.接收数据
while(true){
socket.receive(packet);
//4.获取客户端信息
String clientAddress = packet.getAddress().getHostAddress();
int port = packet.getPort();
System.out.println("收到客服端:"+clientAddress+":"+port+"的消息");
//5.得到数据,由于数据包大小固定,但实际获取到的数据填满bytes,所以需要切片
int length = packet.getLength(); //获取客户端发送数据的字节长度
System.out.println("数据:"+ new String(packet.getData(), 0, length));
System.out.println("--------------------");
}

//6.释放资源 socket.close();
}
}

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class UDPClient {
public static void main(String[] args) throws Exception{
//1.创建客户端对象,未指明端口则随机分配
DatagramSocket socket = new DatagramSocket();
//2.创建数据包,封装要发送出去的数据
/**
* public DatagramPacket(byte buf[], int length,
* InetAddress address, int port)
* 参数一:要发送出去的数据
* 参数二:发送出去的数据大小(字节个数)
* 参数三:服务端IP地址
* 参数四:服务端端口号
*/
Scanner sc = new Scanner(System.in);
while(true){
System.out.println("请输入要发送的信息:");
String msg = sc.nextLine();
byte[] bytes = msg.getBytes();
DatagramPacket packet = new DatagramPacket(bytes, bytes.length,
InetAddress.getLocalHost(), 6666);
//3.发送数据包
socket.send(packet);
System.out.println("------------------------");
}

//释放资源 socket.close();
}
}

image-20250604005050392

image-20250604004925717

2.TCP(传输控制协议)

特点:面向连接、可靠通信

解释:通信前,双方会先采用三次握手的方式建立可靠连接,然后再实现端到端的通信;底层能保证数据成功传给服务端。

多发多收—一服务端一客户端

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class TCPService {
public static void main(String[] args) throws IOException {
//1.创建ServerSocket对象,并为其注册端口
ServerSocket serverSocket = new ServerSocket(6666);
//2.使用serverSocket对象,调用accept方法,等待客户端连接
Socket socket = serverSocket.accept();
//3.从socket通信管道中获取一个字节输入流
InputStream is = socket.getInputStream();
//4.包装成数据流
DataInputStream dis = new DataInputStream(is);
//5.获取数据
while(true){
try{
String s = dis.readUTF();
System.out.println(s);
System.out.println("客户端地址"+socket.getRemoteSocketAddress());
}catch (Exception e){
System.out.println(socket.getRemoteSocketAddress()+"离线了");
socket.close();
dis.close();
break;
}
}


}
}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class TCPClient {
public static void main(String[] args) throws IOException {

//1.创建Socket对象,它默认是由TCP传输协议,host和port是收数据的服务端
Socket socket = new Socket("localhost", 6666);
//2.从Socket通信管道中得到一个字节输出流
OutputStream os =socket.getOutputStream();
//3.把低级的字节输出流包装成数据输出流
DataOutputStream dos = new DataOutputStream(os);

Scanner sc = new Scanner(System.in);
//4.写数据
while(true){
String msg = sc.nextLine();
if(msg.equals("exit")){
dos.close();
socket.close();
break;
}
dos.writeUTF(msg);
//将缓冲区的数据立即写入到目标设备,确保消息能立即发送给服务端
dos.flush();
}


}
}

上面的案例因为只有一个线程来处理请求,所以无法应多个客户端同时发起连接的情况。所以我们可以使用多线程来处理,服务端主线负责异步线程的创建。

支持一服务端与多客户端同时通信

测试时,记得给在idea中设置客户端可以多开

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TCPService {
public static void main(String[] args) throws IOException {
//1.创建ServerSocket对象,并为其注册端口
ServerSocket serverSocket = new ServerSocket(6666);

while(true){
//2.使用serverSocket对象,调用accept方法,等待客户端连接
Socket socket = serverSocket.accept();
System.out.println(socket.getRemoteSocketAddress() + "上线了");
//3.得到连接后,创建异步线程来处理socket
new TCPReadThread(socket).start();
}

}
}

自定义异步线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* TCP异步获取消息线程
*/
public class TCPReadThread extends Thread{
Socket socket;
public TCPReadThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try{
//1.从socket通信管道中获取一个字节输入流
InputStream is = socket.getInputStream();
//2.包装成数据流
DataInputStream dis = new DataInputStream(is);
//3.获取数据
while(true){
try{
String msg = dis.readUTF();
System.out.println(socket.getRemoteSocketAddress() + "消息:" + msg);
}catch (Exception e){
System.out.println(socket.getRemoteSocketAddress() + "下线了");
dis.close();
socket.close();
break;
}
}
}catch (Exception e){
System.out.println("获取输入流异常");
}

}
}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class TCPClient {
public static void main(String[] args) throws IOException {

//1.创建Socket对象,它默认是由TCP传输协议,host和port是收数据的服务端
Socket socket = new Socket("localhost", 6666);
//2.从Socket通信管道中得到一个字节输出流
OutputStream os =socket.getOutputStream();
//3.把低级的字节输出流包装成数据输出流
DataOutputStream dos = new DataOutputStream(os);

Scanner sc = new Scanner(System.in);
//4.写数据
while(true){
String msg = sc.nextLine();
if(msg.equals("exit")){
dos.close();
socket.close();
System.out.println("欢迎您下次光临");
break;
}
dos.writeUTF(msg);
//将缓冲区的数据立即写入到目标设备,确保消息能立即发送给服务端
dos.flush();
}
}
}
即时通讯—群聊

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class TCPClient {
public static void main(String[] args) throws IOException {

//1.创建Socket对象,它默认是由TCP传输协议,host和port是收数据的服务端
Socket socket = new Socket("localhost", 6666);
// 创建消息接收的异步线程
new ClientReadThread(socket).start();
//2.从Socket通信管道中得到一个字节输出流
OutputStream os =socket.getOutputStream();
//3.把低级的字节输出流包装成数据输出流
DataOutputStream dos = new DataOutputStream(os);

Scanner sc = new Scanner(System.in);
//4.写数据
while(true){
String msg = sc.nextLine();
if(msg.equals("exit")){
dos.close();
socket.close();
System.out.println("欢迎您下次光临");
break;
}
dos.writeUTF(msg);
//将缓冲区的数据立即写入到目标设备,确保消息能立即发送给服务端
dos.flush();
}
}
}

客户端消息获取线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class ClientReadThread extends Thread{
Socket socket;
public ClientReadThread(Socket socket){
this.socket = socket;
}

@Override
public void run() {

try {
InputStream is = socket.getInputStream();
DataInputStream dis = new DataInputStream(is);
while(true){
try {
String msg = dis.readUTF();
System.out.println("聊天室:" + msg);
} catch (IOException e) {
dis.close();
socket.close();
break;
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}

}
}

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TCPService {
public static ArrayList<Socket> onLineSocket = new ArrayList<>();
public static void main(String[] args) throws IOException {
//1.创建ServerSocket对象,并为其注册端口
ServerSocket serverSocket = new ServerSocket(6666);

while(true){
//2.使用serverSocket对象,调用accept方法,等待客户端连接
Socket socket = serverSocket.accept();

//加入到在线socket集合中
onLineSocket.add(socket);

//3.得到连接后,创建异步线程来处理socket
new ServiceReadThread(socket).start();
}

}
}

服务端消息获取线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class ServiceReadThread extends Thread{
Socket socket;
public ServiceReadThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try{

System.out.println(socket.getRemoteSocketAddress() + "上线了");
sendToAllClient(socket.getRemoteSocketAddress() + "上线了");
//1.从socket通信管道中获取一个字节输入流
InputStream is = socket.getInputStream();
//2.包装成数据流
DataInputStream dis = new DataInputStream(is);
//3.获取数据
while(true){
String msg;
try{
msg = dis.readUTF();
System.out.println(socket.getRemoteSocketAddress() + "消息:" + msg);
}catch (Exception e){
System.out.println(socket.getRemoteSocketAddress() + "下线了");
sendToAllClient(socket.getRemoteSocketAddress() + "下线了");
dis.close();
socket.close();
break;
}
//消息分发给其他客户端
sendToAllClient(socket.getRemoteSocketAddress() + ": "+msg);
}
}catch (Exception e){
System.out.println("获取输入流异常");
}
//从在线socket中移除
TCPService.onLineSocket.remove(socket);
}

private void sendToAllClient(String msg) throws IOException {
for (Socket sc : TCPService.onLineSocket) {
if(!sc.getRemoteSocketAddress().toString().equals(socket.getRemoteSocketAddress().toString())){
OutputStream os = sc.getOutputStream();
DataOutputStream dos = new DataOutputStream(os);
//发送至其他客户端的消息管道
dos.writeUTF(msg);
dos.flush();
}
}
}
}
BS架构请求

浏览器也可以对本地开放的端口进行访问,这时就不要客户端了,但返回给浏览器的数据必须要严格按照浏览器请求的协议接收的数据格式进行返回。

image-20250821152219269

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class TCPService {
//使用线程池对线程进行管理,避免线程资源耗尽
public static ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 5, 1000, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());


public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8080);
while(true){
//获取浏览器的socket连接
Socket socket = serverSocket.accept();
//将socket连接交给线程池进行处理
// pool.submit(new ServerReadThread(socket));
pool.execute(new ServerReadThread(socket));
}
}
}

服务端获取数据线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class ServerReadThread extends Thread{
Socket socket;
public ServerReadThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
System.out.println(socket.getRemoteSocketAddress() + "发起请求");
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=16px;text-align:center'> ldy你好 </div>");
//浏览器一般是短连接,浏览器发起请求,服务端响应请求之后连接就结束了,所以这种情况下就可以直接close了
ps.close();//及时释放系统资源
socket.close();//及时释放系统资源
System.out.println(socket.getRemoteSocketAddress() + "请求已关闭");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

二、面试知识

TCP三次握手

三次握手:为了确保双方收发消息都没问题

假设客户端发出连接请求

第一次握手:客户端发出连接请求,服务端收到请求后,知道客户端发消息没问题。

第二次握手:服务端返回一个响应。客户端收到响应后,知道服务端收消息和发消息都没问题。

第三次握手:客户端再次发送确认信息给服务端。服务端收到消息后,知道客户端接收消息没问题。

这样,经过三次,客户端和服务端都知道对方可以正常收发消息,就可以开始进行通信了。

image-20250821133631120

TCP四次挥手

四次挥手:确保双方的数据收发都已经完毕,保证断开的可靠性

第一次挥手:客户端发起断开连接请求

第二次挥手:服务器端可能有客户端的请求还在处理,所以先返回一个稍等的响应

第三次挥手:服务器端把数据处理完毕之后,确定自己没有数据要收和发了之后,返回确认断开的响应

第四次挥手:客户端在处理完毕服务器端的数据后,确定自己没有数据要收和发了,处理完成后,发出正式确认断开连接的请求。