所谓的网络资源,其实就是在网络中可以获取的各种数据资源,而所有的网络资源,都是通过网络编程来进行数据传输的。
网络资源包括:视频资源,图片资源,文本资源等等
用户在浏览器中,打开在线视频网站,如在 b 站看视频,实质是通过网络,获取到网络上的一个视频资源,与本地打开视频文件类似,只是视频文件这个资源的来源是网络。

那么我们怎么将这些网络资源在不同的设备上进行传输呢?答案是网络编程
网络编程:指网络上的主机,通过不同的进程,以编程的方式实现网络通信(或称为网络数据传输)。

当然,我们只要满足进程不同就行;所以即便是同一个主机,只要是不同进程,基于网络来传输数据,也属于网络编程。
对于开发来说,在条件有限的情况下,一般也都是在一个主机中运行多个进程来完成网络编程。
但是我们一定要明确我们的目的是提供网络上不同主机,基于网络来传输数据资源:
在一次网络数据传输时:
注意:发送端和接收端只是相对的,只是一次网络数据传输产生数据流向后的概念。

一般来说,获取一个网络资源,涉及到两次网络数据传输:
好比在快餐店点一份炒饭:先要发起请求:点一份炒饭,
再有快餐店提供的对应响应:提供一份炒饭

对于服务来说,一般有两种:


最常见的场景,客户端是指给用户使用的程序,服务端是提供用户服务的程序:

因为应用层获得数据需要通过传输层传输数据,所以我们要通过应用层调用传输层,而传输层也为应用层提供了 socket api,用于应用层调用传输层,
这个api有两组:
因为TCP和UDP的差别很大,所以这两组api的差别也很大。
TCP,即Transmission Control Protocol(传输控制协议)
UDP,即User Datagram Protocol(用户数据报协议)
TCP 和 UDP都是传输层协议,但是TCP比UDP复杂,因为TCP要保证可靠性,同时又尽可能的提高性能,因为TCP的机制,不管TCP怎么提高效率,TCP还是不如UDP的
下面说说这些特点的含义是什么:
有连接是指在数据传输之前,发送方与接收方需要先建立一个可靠的通信链路,以确保双方能够互相通信。在传输过程中,双方需要维护一定的状态信息。
而无连接则是指在数据传输之前,发送方和接收方之间不需要建立可靠的通信链路。每个数据包都是独立传输的,不关心之前或之后的数据包是否丢失。
可靠传输是指数据在传输过程中会被发送方和接收方进行确认,并进行丢失、错误、重复等错误处理,确保数据的完整性和正确性。发送方会等待接收方的确认信息,如果没有得到确认,会重新发送数据。
不可靠传输则是指数据在传输过程中不会进行确认,不保证数据的完整性和正确性。
面向字节流是指数据在传输过程中被看作是一连串的无结构的字节流,发送方按照字节流进行传输,接收方按照相同的字节流进行接收,不考虑数据的边界或分段。
面向数据报则是指数据在传输过程中被分割成数据报的形式进行传输,每个数据报都包含了数据的完整信息,接收方根据数据报的边界进行接收和处理。
全双工是指发送方和接收方能够同时进行数据的发送和接收,可以进行双向通信,彼此不会干扰对方,发送方和接收方之间的通信是完全独立的。例如:在电话通信中,发送方和接收方可以同时说话和倾听。
半双工则是指发送方和接收方不能同时进行数据的发送和接收,每个时刻只能进行单向通信。发送方和接收方之间的通信是互斥的。例如:在对讲机通信中,一个人说话时,另一个人只能倾听。
DatagramSocket 是UDP Socket,用于发送和接收UDP数据报。
DatagramSocket 构造方法:
| 方法签名 | 方法说明 |
|---|---|
| DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口 |
| DatagramSocket(int port) | 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口 |
DatagramSocket 方法:
| 方法签名 | 方法说明 |
|---|---|
| void receive(DatagramPacket p) | 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) |
| void send(DatagramPacket p) | 从此套接字发送数据报包(不会阻塞等待,直接发送) |
| void close() | 关闭此数据报套接字 |
DatagramPacket是UDP Socket发送和接收的数据报。
DatagramPacket 构造方法:
| 方法签名 | 方法说明 |
|---|---|
| DatagramPacket(byte[] buf, int length) | 构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组中,接收指定长度 |
| DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) | 构造一个DatagramPacket以用来发送数据报,发送的数据为字节数组中,从0到指定长度。address指定目的主机的IP和端口号 |
DatagramPacket 方法:
| 方法签名 | 方法说明 |
|---|---|
| InetAddress.getAddress() | 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址 |
| InetAddress.getPort() | 从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号 |
| DatagramPacket.getData() | 获取数据报中的数据 |
构造UDP发送的数据报时,需要传入 SocketAddress ,该对象可以使用InetSocketAddress 来创建。
InetSocketAddress ( SocketAddress 的子类 )构造方法:
| 方法签名 | 方法说明 |
|---|---|
| InetSocketAddress(InetAddress addr, int port) | 创建一个Socket地址,包含IP地址和端口号 |
一发一收(无响应)
package org.example.udp.demo1;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.util.Arrays;
public class UdpServer {
//服务器socket要绑定固定的端口
private static final int PORT = 8888;
public static void main(String[] args) throws IOException {
// 1.创建服务端DatagramSocket,指定端口,可以发送及接收UDP数据报
DatagramSocket socket = new DatagramSocket(PORT);
//不停的接收客户端udp数据报
while (true){
// 2.创建数据报,用于接收客户端发送的数据
byte[] bytes = new byte[1024];//1m=1024kb, 1kb=1024byte, UDP最多64k(包含UDP首部8byte)
DatagramPacket packet = new DatagramPacket(bytes, bytes.length);
System.out.println("------------------------------------------------
---");
System.out.println("等待接收UDP数据报...");
// 3.等待接收客户端发送的UDP数据报,该方法在接收到数据报之前会一直阻塞,接收到数据报以后,DatagramPacket对象,包含数据(bytes)和客户端ip、端口号
socket.receive(packet);
System.out.printf("客户端IP:%s%n",packet.getAddress().getHostAddress());
System.out.printf("客户端端口号:%s%n", packet.getPort());
System.out.printf("客户端发送的原生数据为:%s%n", Arrays.toString(packet.getData()));
System.out.printf("客户端发送的文本数据为:%s%n", new String(packet.getData()));
}
}
}
运行后,服务端就启动了,控制台输出如下:
等待接收UDP数据报…
可以看出,此时代码是阻塞等待在 socket.receive(packet) 代码行,直到接收到一个UDP数据报。
package org.example.udp.demo1;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
public class UdpClient {
// 服务端socket地址,包含域名或IP,及端口号
private static final SocketAddress ADDRESS = new InetSocketAddress("localhost", 8888);
public static void main(String[] args) throws IOException {
// 4.创建客户端DatagramSocket,开启随机端口就行,可以发送及接收UDP数据报
DatagramSocket socket = new DatagramSocket();
// 5-1.准备要发送的数据
byte[] bytes = "hello world!".getBytes();
// 5-2.组装要发送的UDP数据报,包含数据,及发送的服务端信息(服务器IP+端口号)
DatagramPacket packet = new DatagramPacket(bytes, bytes.length,ADDRESS);
// 6.发送UDP数据报
socket.send(packet);
}
}
客户端启动后会发送一个"hello world!" 的字符串到服务端,在服务端接收后,控制台输出内容如下:
等待接收UDP数据报…
客户端IP:127.0.0.1
客户端端口号:57910
客户端发送的原生数据为:[104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33,
0, 0, 0, …此处省略很多0]
客户端发送的文本数据为:hello world!
等待接收UDP数据报…
从以上可以看出,发送的UDP数据报,在接收到以后(假设接收的数据字节数组长度为N,假设发送的数据字节数组长度为M):
要解决以上问题,就需要发送端和接收端双方约定好一致的协议,如规定好结束的标识或整个数据的长度。
ServerSocket 是创建TCP服务端Socket的API。
ServerSocket 构造方法:
| 方法签名 | 方法说明 |
|---|---|
| ServerSocket(int port) | 创建一个服务端流套接字Socket,并绑定到指定端口。 |
ServerSocket 方法:
| 方法签名 | 方法说明 |
|---|---|
| Socket accept() | 开始监听创建时绑定的端口,有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待 |
| void close() | 关闭此套接字 |
Socket 构造方法:
| 方法签名 | 方法说明 |
|---|---|
| Socket(String host, int port) | 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接 |
Socket 方法:
| 方法签名 | 方法说明 |
|---|---|
| getInetAddress() | 返回套接字所连接的地址 |
| getInputStream() | 返回此套接字的输入流 |
| getOutputStream() | 返回此套接字的输出流 |
一发一收(短连接)
package org.example.tcp.demo1;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class TcpServer {
//服务器socket要绑定固定的端口
private static final int PORT = 8888;
public static void main(String[] args) throws IOException {
// 1.创建一个服务端ServerSocket,用于收发TCP报文
ServerSocket server = new ServerSocket(PORT);
// 不停的等待客户端连接
while(true) {
System.out.println("---------------------------------------------------");
System.out.println("等待客户端建立TCP连接...");
// 2.等待客户端连接,注意该方法为阻塞方法
Socket client = server.accept();
System.out.printf("客户端IP:%s%n",client.getInetAddress().getHostAddress());
System.out.printf("客户端端口号:%s%n", client.getPort());
// 5.接收客户端的数据,需要从客户端Socket中的输入流获取
System.out.println("接收到客户端请求:");
InputStream is = client.getInputStream();
// 为了方便获取字符串内容,可以将以上字节流包装为字符流
BufferedReader br = new BufferedReader(new InputStreamReader(is,"UTF-8"));
String line;
// 一直读取到流结束:TCP是基于流的数据传输,一定要客户端关闭Socket输出流才表示服务端接收IO输入流结束
while ((line = br.readLine()) != null) {
System.out.println(line);
}
// 6.双方关闭连接:服务端是关闭客户端socket连接
client.close();
}
}
}
运行后,服务端就启动了,控制台输出如下:
等待客户端建立TCP连接…
可以看出,此时代码是阻塞等待在 server.accept() 代码行,直到有新的客户端申请建立连接。
package org.example.tcp.demo1;
import java.io.*;
import java.net.Socket;
public class TcpClient {
//服务端IP或域名
private static final String SERVER_HOST = "localhost";
//服务端Socket进程的端口号
private static final int SERVER_PORT = 8888;
public static void main(String[] args) throws IOException {
// 3.创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接
Socket client = new Socket(SERVER_HOST, SERVER_PORT);
// 4.发送TCP数据,是通过socket中的输出流进行发送
OutputStream os = client.getOutputStream();
// 为了方便输出字符串作为发送的内容,可以将以上字节流包装为字符流
PrintWriter pw = new PrintWriter(new OutputStreamWriter(os, "UTF-8"));
// 4-1.发送数据:
pw.println("hello world!");
// 4-2.有缓冲区的IO操作,真正传输数据,需要刷新缓冲区
pw.flush();
// 7.双方关闭连接:客户端关闭socket连接
client.close();
}
}
客户端启动后会发送一个"hello world!" 的字符串到服务端,在服务端接收后,控制台输出内容如下:
等待客户端建立TCP连接…
客户端IP:127.0.0.1
客户端端口号:51118
接收到客户端请求:
hello world!
等待客户端建立TCP连接…
以上客户端与服务端建立的为短连接,每次客户端发送了TCP报文,及服务端接收了TCP报文后,双方都会关闭连接。
TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接:
也就是说,短连接只能一次收发数据。
也就是说,长连接可以多次收发数据。


如果一个进程A已经绑定了一个端口,再启动一个进程B绑定该端口,就会报错,这种情况也叫端口被占用。对于java进程来说,端口被占用的常见报错信息如下:

此时需要检查进程B绑定的是哪个端口,再查看该端口被哪个进程占用。以下为通过端口号查进程的方式:


解决端口被占用的问题:
以上我们实现的UDP和TCP数据传输,除了UDP和TCP协议外,程序还存在应用层自定义协议,可以想想分别都是什么样的协议格式。
对于客户端及服务端应用程序来说,请求和响应,需要约定一致的数据格式:
约定相同的数据格式,主要目的是为了让接收端在解析的时候明确如何解析数据中的各个字段。
可以使用知名协议(广泛使用的协议格式),如果想自己约定数据格式,就属于自定义协议。
对发送数据时的数据包装动作来说:
接收数据时的数据解析动作来说: