Java网络编程

网络编程

基本的通信架构有2种形式:CS架构(Client客户端/Server服务端) 、BS架构(Browser浏览器/Server服务端)。

CS架构:

  • 客户端:需要程序员开发。用户需要安装。
  • 服务端:需要程序员开发实现。

BS架构

  • 客户端:不需要程序员开发实现。用户需要安装浏览器。
  • 服务端:需要程序员开发实现。

网络通信的三要素:

  1. IP:设备在网络中的地址,是唯一的标识。

    192.168. 开头的就是常见的局域网地址,范围即为192.168.0.0192.168.255.255,专门为组织机构内部使用。

    127.0.0.1localhost:代表本机IP,只会寻找当前所在的主机。

    ipconfig:查看本机IP地址。

    ping IP地址:检查网络是否连通。

  2. 端口:应用程序在设备中唯一的标识。

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

    分类:

    周知端口:0~1023,被预先定义的知名应用占用(如:HTTP占用 80,FTP占用21) 。

    注册端口:1024~49151,分配给用户进程或某些应用程序。

    动态端口:49152~65535,之所以称为动态端口,是因为它一般不固定分配某种进程,而是动态分配。

    注意:自己开发的程序一般选择使用注册端口,且一个设备中不能出现两个程序的端口号一样,否则出错。

  3. 协议:连接和数据在网络中传输的规则。

    UDP(User Datagram Protocol):用户数据报协议。无连接、不可靠通信。一次最多发送64K数据

    TCP(Transmission Control Protocol):传输控制协议。面向连接、可靠通信(三次握手建立连接,传输数据进行确认,四次挥手断开连接)。

InetAddress

InetAddress代表IP地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) throws Exception {
InetAddress ip1 = InetAddress.getLocalHost();//获取本机IP地址对象
System.out.println(ip1.getHostName());
System.out.println(ip1.getHostAddress());

//获取指定IP或者域名的IP地址对象
InetAddress ip2 = InetAddress.getByName("www.baidu.com");
System.out.println(ip2.getHostName());
System.out.println(ip2.getHostAddress());

//相当于:ping www.baidu.com
System.out.println(ip2.isReachable(6000));//6s内是否连通
}

UDP通信

特点:无连接、不可靠通信。不事先建立连接;发送端每次把要发送的数据(限制在64KB内)、接收端IP、等信息封装成一个数据包,发出去就不管了。

DatagramSocket、DatagramPacket

Java提供了一个java.net.DatagramSocket类来实现UDP通信。

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
//UDP客户端
public class UDP_Client {
public static void main(String[] args) throws Exception {
//1.创建客户端对象
DatagramSocket socket = new DatagramSocket();//随意分配端口,也可以指定端口
Scanner sc = new Scanner(System.in);
while(true) {
System.out.println("请发送数据:");
String msg = sc.nextLine();
if("exit".equals(msg)) {
System.out.println("成功退出");
socket.close();//关闭客户端对象
break;
}
byte[] bytes = msg.getBytes();

//2.创建数据包对象封装成要发出去的数据
DatagramPacket packet = new DatagramPacket(bytes, //封装要发出去的数据
bytes.length, //发送出去的数据大小(字节个数)
InetAddress.getLocalHost(), //服务器的IP地址(找到服务器主机)
6666); //服务器程序的端口

//3.发送数据包的数据
socket.send(packet);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//UDP服务端
public class UDP_Server {
public static void main(String[] args) throws Exception {
System.out.println("启动服务器");
//1.创建服务器对象,注册端口
DatagramSocket socket = new DatagramSocket(6666);

//2.创建数据包对象,用于接收数据
byte[] bytes = new byte[1024 * 64];//一次发送的数据不会超过64KB
DatagramPacket packet = new DatagramPacket(bytes, bytes.length);

while(true) {
//3.使用数据包接收客户端发送的数据
socket.receive(packet);//服务端会阻塞等待客户端发送数据
int len = packet.getLength();//接收数据的长度
String rs = new String(packet.getData(), 0, len);
System.out.println("数据:" + rs);
System.out.println("IP地址:" +packet.getAddress().getHostAddress());//获取客服的IP地址
System.out.println("端口号:" + packet.getPort());//获取客户端端口
System.out.println("-----------------------");
}
//socket.close();//释放资源(但是服务员会一直运行,不用释放资源)
}
}

TCP通信

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

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

Java提供了一个java.net.Socket类来实现TCP通信。

客户端Socket

客户端程序是通过java.net包下的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
26
//TCP客户端
public class TCP_Client {
public static void main(String[] args) throws Exception {
//1.创建Socket对象,请求与服务器程序连接
Socket socket = new Socket("127.0.0.1", 8888);
//2.从Socket通信管道中得到一个字节输出流,用来发送数据给服务器程序
OutputStream os = socket.getOutputStream();
//3.把低级的字节输出流包装成数据输出流(易于写数据)
DataOutputStream dos = new DataOutputStream(os);

Scanner sc = new Scanner(System.in);
while(true) {
System.out.println("请发送数据:");
String msg = sc.nextLine();
if("exit".equals(msg)) {
System.out.println("成功退出");
dos.close();//关闭字节输出流
socket.close();//关闭客户端对象
break;
}
//4.写数据出去
dos.writeUTF(msg);
dos.flush();//需要把数据刷新出去,防止数据还在客户端的内存中
}
}
}
服务端ServerSocket

服务端是通过java.net包下的ServerSocket类来实现的。

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
//TCP服务端
public class TCP_Server {
public static void main(String[] args) throws Exception {
System.out.println("启动服务器");
//1.创建ServerSocket对象,同时为服务器注册端口
ServerSocket serverSocket = new ServerSocket(8888);
//2.使用ServerSocket对象,调用accept方法,等待客户端的连接请求
Socket socket = serverSocket.accept();
//3.从socket通信管道中得到字节输入流
InputStream is = socket.getInputStream();
//4.把原始的字节输入流包装成数据输入流(易于读数据)
DataInputStream dis = new DataInputStream(is);
while(true) {
try {
//5.使用数据输入流读取客户端发送过来的消息
String rs = dis.readUTF();//这里服务端会阻塞等待客户端发消息,如果客户端主动退出,这里会报错:java.io.EOFException
//若客服端比服务端快,服务端底层会缓存消息,直到这里服务端readUTF读取消息
System.out.println(rs);
} catch (Exception e) {
System.out.println(socket.getRemoteSocketAddress() + "离线了");//拿到远程客户端地址
dis.close();//
socket.close();
break;
}
}
}
}
与多个客户端同时通信

方法:主线程定义循环负责接收客户端Socket管道连接。每接收到一个Socket通信管道后分配一个独立的线程负责处理它。

1
//客户端使用上述TCP_Client即可
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
//ServerReaderThread,独立的线程处理
public class ServerReaderThread extends Thread {
private Socket socket;
public ServerReaderThread(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 (Exception e) {
System.out.println("下线:" + socket.getRemoteSocketAddress());
dis.close();
socket.close();
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//服务端TCP_Server2
public class TCP_Server2 {//对应使用TCP_Client就好了
public static void main(String[] args) throws Exception {
System.out.println("启动服务器");
ServerSocket serverSocket = new ServerSocket(8888);

while(true) {
//使用ServerSocket对象,调用accept方法,等待客户端的连接请求
Socket socket = serverSocket.accept();
System.out.println("上线:" + socket.getRemoteSocketAddress());
//把这个客服端对应的Socket通信管道交给一个独立的线程处理
new ServerReaderThread(socket).start();
}
}
}
案例:群聊

群聊是指一个客户端把消息发出去,其他在线的全部客户端都可以收到消息。

方法:需要用到端口转发的设计思想。服务端需要把在线的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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//服务端线程:处理每个客户端的连接
public class ServerChatThread extends Thread {
private Socket socket;
public ServerChatThread(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);
//把消息分发给全部客户端接收(包括自己)
sendMsgToAll(msg);
} catch (Exception e) {
System.out.println("下线:" + socket.getRemoteSocketAddress());
Chat_Server.onLineSockets.remove(socket);//移除离线的Socket
dis.close();
socket.close();
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}

private void sendMsgToAll(String msg) throws Exception {
//发送给全部在线的Socket管道接收
for(Socket onLineSocket : Chat_Server.onLineSockets) {
OutputStream os = onLineSocket.getOutputStream();
DataOutputStream dos = new DataOutputStream(os);
dos.writeUTF(msg);
dos.flush();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//服务端
public class Chat_Server {
public static List<Socket> onLineSockets = new ArrayList<>();//存储在线客户端Socket

public static void main(String[] args) throws Exception {
System.out.println("启动服务器");
ServerSocket serverSocket = new ServerSocket(8888);

while(true) {
//使用ServerSocket对象,调用accept方法,等待客户端的连接请求
Socket socket = serverSocket.accept();
onLineSockets.add(socket);
System.out.println("上线:" + socket.getRemoteSocketAddress());
//把这个客服端对应的Socket通信管道交给一个独立的线程处理
new ServerChatThread(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
//客户端线程:接收服务端传回来的数据(和服务端线程接收数据的run方法类似)
public class ClientChatThread extends Thread {
private Socket socket;
public ClientChatThread(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 (Exception e) {
System.out.println("自己下线:" + socket.getRemoteSocketAddress());
dis.close();
socket.close();
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
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 Chat_Client {
public static void main(String[] args) throws Exception {
Socket socket = new Socket("127.0.0.1", 8888);
//创建独立的线程,从Socket中接收服务器发送过来的消息
new ClientChatThread(socket).start();

OutputStream os = socket.getOutputStream();
DataOutputStream dos = new DataOutputStream(os);

Scanner sc = new Scanner(System.in);
while(true) {
System.out.println("请发送数据:");
String msg = sc.nextLine();
if("exit".equals(msg)) {
System.out.println("成功退出");
dos.close();//关闭字节输出流
socket.close();//关闭客户端对象
break;
}
dos.writeUTF(msg);
dos.flush();//需要把数据刷新出去,防止数据还在客户端的内存中
}
}
}
案例:BS架构

基本原理:客户端使用浏览器发起请求(不需要开发客户端)。服务端必须按照HTTP协议响应数据。

注意:服务器必须给浏览器响应HTTP协议规定的数据格式,否则浏览器不识别返回的数据。

HTTP协议规定:响应给浏览器的数据格式必须满足如下格式。

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
//服务端线程
public class ServerBSThread extends Thread {
private Socket socket;
public ServerBSThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
//立即响应一个网页内容给浏览器展示
try {
OutputStream os = socket.getOutputStream();

/*
DataOutputStream dos = new DataOutputStream(os);
dos.writeUTF("Hello ServerBSThread");
dos.close();
//使用这种方式浏览器无法识别,会显示:127.0.0.1 发送了无效的响应。必须要遵守HTTP协议,浏览器才能识别。
*/

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'> surourou666 </div>");

socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//服务端
public class BS_Server {
public static void main(String[] args) throws Exception {
System.out.println("启动服务器");
ServerSocket serverSocket = new ServerSocket(8888);

while(true) {
//使用ServerSocket对象,调用accept方法,等待客户端的连接请求
Socket socket = serverSocket.accept();
System.out.println("上线:" + socket.getRemoteSocketAddress());
//把这个客服端对应的Socket通信管道交给一个独立的线程处理
new ServerBSThread(socket).start();
}
}
}
案例:BS架构(使用线程池优化)

每次请求都开一个新线程,在高并发时,容易引起宕机,所以使用线程池进行优化。

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
//服务端线程,和上面的ServerBSThread内容是一样的。
//区别:ServerBSThread继承了Thread类,ServerBSpoolRunnable实现了Runnable接口,便于线程池使用。
public class ServerBSpoolRunnable implements Runnable {
private Socket socket;
public ServerBSpoolRunnable(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
//立即响应一个网页内容给浏览器展示
try {
OutputStream os = socket.getOutputStream();

/*
DataOutputStream dos = new DataOutputStream(os);
dos.writeUTF("Hello ServerBSThread");
dos.close();
//使用这种方式浏览器无法识别,会显示:127.0.0.1 发送了无效的响应。必须要遵守HTTP协议,浏览器才能识别。
*/

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'> surourou666 </div>");

socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class BSpool_Server {
public static void main(String[] args) throws Exception {
System.out.println("启动服务器");
ServerSocket serverSocket = new ServerSocket(8888);

//创建一个线程池,负责处理通信管道的任务
ThreadPoolExecutor pool = new ThreadPoolExecutor(16*2, 16*2, 0, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(8),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());

while(true) {
//使用ServerSocket对象,调用accept方法,等待客户端的连接请求
Socket socket = serverSocket.accept();
System.out.println("上线:" + socket.getRemoteSocketAddress());

pool.execute(new ServerBSpoolRunnable(socket));//使用线程池处理任务
}
}
}

Java网络编程
http://surourou8.github.io/2024/09/30/Java网络编程/
作者
Su Rourou
发布于
2024年9月30日
许可协议