文章目录
-
- [一、 网络编程基础](#一、 网络编程基础)
-
- [1.1 网络编程中的基本概念](#1.1 网络编程中的基本概念)
-
- [1.1.1 发送端和接收端](#1.1.1 发送端和接收端)
- [1.1.2 请求和响应](#1.1.2 请求和响应)
- [1.1.3 客户端和服务端](#1.1.3 客户端和服务端)
- 二、Socket套接字
- 三、UDP数据报套接字编程
- 四、TCP流套接字编程
-
-
- [4.1 服务器引入多线程](#4.1 服务器引入多线程)
- [4.2 服务器引入线程池](#4.2 服务器引入线程池)
-
一、 网络编程基础
- 什么是网络编程
网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络通信(或称为网络数据传输)。
只要满足进程不同就行;所以即便是同一个主机,只要是不同进程,基于网络来传输数据,也属于网络编程。
特殊的,对于开发来说,在条件有限的情况下,一般也都是在一个主机中运行多个进程来完成网络编程。
1.1 网络编程中的基本概念
1.1.1 发送端和接收端
在一次网络数据传输时:
- 发送端:数据的发送方进程,称为发送端。发送端主机即网络通信中的源主机。
- 接收端:数据的接收方进程,称为接收端。接收端主机即网络通信中的目的主机。
- 收发端:发送端和接收端两端,也简称为收发端。
注意:发送端和接收端只是相对的,只是一次网络数据传输产生数据流向后的概念。
1.1.2 请求和响应
一般来说,获取一个网络资源,涉及到两次网络数据传输:
- 第一次:请求数据的发送
- 第二次:响应数据的发送

1.1.3 客户端和服务端
- 服务端:在常见的网络数据传输场景下,把提供服务的一方进程,称为服务端,可以提供对外服务。
- 客户端:获取服务的一方进程,称为客户端。
对于服务来说,一般是提供:
-
客户端获取服务资源
例如:客户端主机的客户端进程发送"获取服务资源"的请求到服务端主机的服务端进程,服务端进程发送"返回服务资源"的响应到客户端进程,如:传输视频资源、图片资源、文本资源。
-
客户端保存资源在服务端
例如:客户端主机的客户端进程发送"保存用户资源"的请求到服务端主机的服务端进程,服务端进程保存资源后发送"返回处理结果"的响应到客户端进程。
二、Socket套接字
概念: Socket套接字,是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。基于Socket套接字的网络程序开发就是网络编程。
分类: Socket套接字主要针对传输层协议划分为如下三类:
- 流套接字:使用传输层TCP协议
TCP,即(传输控制协议),传输层协议。
以下为TCP的特点:
- 有连接
- 可靠传输
- 面向字节流
- 有接收缓冲区,也有发送缓冲区
- 大小不限
对于字节流来说,可以简单的理解为,传输数据是基于IO流,流式数据的特征就是在IO流没有关闭的情况下,是无边界的数据,可以多次发送,也可以分开多次接收。
- 数据报套接字:使用传输层UDP协议
UDP,即(用户数据报协议),传输层协议。
以下为UDP的特点:
- 无连接
- 不可靠传输
- 面向数据报
- 有接收缓冲区,无发送缓冲区
- 大小受限:一次最多传输64k
对于数据报来说,可以简单的理解为,传输数据是一块一块的,发送一块数据(假如100个字节),必须一次发送,接收也必须一次接收100个字节,而不能分100次,每次接收1个字节。
- 原始套接字
原始套接字用于自定义传输层协议,用于读写内核没有处理的IP协议数据。
- Socket编程注意事项
- 客户端和服务端:开发时,经常是基于一个主机开启两个进程作为客户端和服务端,但真实的场景,一般都是不同主机。
- 注意目的IP和目的端口号,标识了一次数据传输时要发送数据的终点主机和进程。
- Socket编程是使用流套接字和数据报套接字,基于传输层的TCP或UDP协议,但应用层协议,也需要考虑。
- 关于端口被占用的问题
- 如果一个进程A已经绑定了一个端口,再启动一个进程B绑定该端口,就会报错,这种情况也叫端口被占用。对于java进程来说,端口被占用的常见报错信息如下:

三、UDP数据报套接字编程
API 介绍
- DatagramSocket :
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 :
DatagramPacket是UDP Socket发送和接收的数据报。
- DatagramPacket 构造方法:
| 方法签名 | 方法说明 |
|---|---|
DatagramPacket(byte[] buf, int length) |
构造一个 DatagramPacket 以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数length) |
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) |
构造一个 DatagramPacket 以用来发送数据报,发送的数据为字节数组(第一个参数buf)中,从0到指定长度(第二个参数length)。address指定目的主机的IP和端口号 |
- DatagramPacket 方法:
| 方法签名 | 方法说明 |
|---|---|
InetAddress getAddress() |
从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址 |
int getPort() |
从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号 |
byte[] getData() |
获取数据报中的数据 |
- 构造UDP发送的数据报时,需要传入
SocketAddress,该对象可以使用InetSocketAddress来创建。
- InetSocketAddress:
InetSocketAddress(SocketAddress的子类)构造方法:
| 方法签名 | 方法说明 |
|---|---|
InetSocketAddress(InetAddress addr, int port) |
创建一个Socket地址,包含IP地址和端口号 |
代码示例:
- UDP Echo Server
java
//服务器
public class UdpEchoServer {
//首先定义socket对象
private DatagramSocket socket = null;
/**
*
* @param port 端口号:区分一个主机上的不同的应用程序
* @throws SocketException
*/
public UdpEchoServer(int port) throws SocketException {
socket= new DatagramSocket(port);
}
/**
* 启动服务器
*/
public void start() throws IOException {
System.out.println("server start!");
//死循环:编写服务器的一种常见做法,不断处理客服端发来的请求
while (true){
//1. 读取请求并解析
DatagramPacket requestPacket = new DatagramPacket(new byte[1024],1024);
socket.receive(requestPacket);
//二进制转字符串
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
//2. 根据请求计算响应。(注:通常是一个复杂的过程。此处只做回显数据)
String response = process(request);
//3. 将响应返回给客户端
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),
response.getBytes().length,requestPacket.getSocketAddress());
socket.send(responsePacket);
//打印日志
System.out.printf("[%s:%d] req: %s, resp: %s\n",requestPacket.getAddress().toString(),
requestPacket.getPort(),request,response);
}
}
//根据请求计算响应
public String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}
- UDP Echo Client
java
//客户端
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIp;
private int serverPort;
/**
* 此处和服务器不一样,不用指定端口号
* 意味着操作系统会自动分配一个空闲的端口号
* @param serverIp : 服务器Ip
* @param serverPort : 服务器端口号
* @throws SocketException
*/
public UdpEchoClient(String serverIp,int serverPort) throws SocketException {
socket = new DatagramSocket();
this.serverIp = serverIp;
this.serverPort = serverPort;
}
public void start() throws IOException {
System.out.println("client start!");
Scanner scanner = new Scanner(System.in);
//输入字符串发给服务器,从服务器读取响应
while (true){
// 1. 从控制台读取用户输入
System.out.print("->");
String request = scanner.next();
if(request.equals("exit")){
break;
}
// 2. 将用户输入的字符串构造出UDP数据报进行发送
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(this.serverIp),this.serverPort);
socket.send(requestPacket);
// 3. 从服务器读取响应
DatagramPacket responsePacket = new DatagramPacket(new byte[1024],1024);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(),0,responsePacket.getLength());
// 4. 显示响应
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);
client.start();
}
}
- UDP Dict Server
编写一个英译汉的服务器,只需要重写 process 方法:
java
/**
* 英译汉服务器
*/
public class UdpDictServer extends UdpEchoServer{
private Map<String,String> dict = new HashMap<String,String>();
/**
* @param port 端口号:区分一个主机上的不同的应用程序
* @throws SocketException
*/
public UdpDictServer(int port) throws SocketException {
super(port);
dict.put("hello","你好");
dict.put("world","世界");
dict.put("cat","小猫");
dict.put("dog","小狗");
dict.put("pig","小猪");
}
@Override
public String process(String request){
return dict.getOrDefault(request,"没有找到该单词");
}
public static void main(String[] args) throws IOException {
UdpDictServer server = new UdpDictServer(9090);;
server.start();
}
}
四、TCP流套接字编程
和刚才UDP类似,实现一个简单的英译汉的功能。
API 介绍
- ServerSocket:
ServerSocket是创建TCP服务端Socket的API。
- ServerSocket 构造方法:
| 方法签名 | 方法说明 |
|---|---|
ServerSocket(int port) |
创建一个服务端流套接字Socket,并绑定到指定端口 |
- ServerSocket 方法:
| 方法签名 | 方法说明 |
|---|---|
Socket accept() |
开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待 |
void close() |
关闭此套接字 |
- Socket:
Socket是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。
不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。
- Socket 构造方法:
| 方法签名 | 方法说明 |
|---|---|
Socket(String host, int port) |
创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接 |
- Socket 方法:
| 方法签名 | 方法说明 |
|---|---|
InetAddress getInetAddress() |
返回套接字所连接的地址 |
InputStream getInputStream() |
返回此套接字的输入流 |
OutputStream getOutputStream() |
返回此套接字的输出流 |
代码示例
- TCP Echo Server
java
public class TcpEchoServer {
ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("server start");
while (true){
// 首先接收客户端的连接,然后才能进行通信
// 如果有客户端和服务器建立好了连接,accept 才能返回
// 否则 accept 会阻塞
Socket socket = serverSocket.accept();
// 通过这个方法处理这个客服端整个的连接过程
processConnection(socket);
}
}
private void processConnection(Socket socket) {
System.out.printf("[%s:%d] 客户端上线!\n",socket.getInetAddress(),socket.getPort());
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
) {
Scanner scanner = new Scanner(inputStream);
PrintWriter writer = new PrintWriter(outputStream);
while (true){
// 处理多次请求/响应的操作
//1. 读取请求并解析
if(!scanner.hasNext()){
System.out.printf("[%s:%d] 客户端下线!\n",socket.getInetAddress(),socket.getPort());
break;
}
String request = scanner.next();
//2. 根据请求计算响应
String response = process(request);
//3. 把响应写回客户端
writer.println(response);
writer.flush();
//4. 打印日志
System.out.printf("[%s:%d] res: %s,resp: %s\n",socket.getInetAddress(),socket.getPort(),
request,response);
}
}catch (IOException e){
e.printStackTrace();
}finally {
try {
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
- TCP Echo Client
java
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String serverIp,int serverPort) throws IOException {
socket = new Socket(serverIp,serverPort);
}
public void start(){
System.out.println("client start");
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
) {
Scanner scanner = new Scanner(System.in);
Scanner scannerNetwork = new Scanner(inputStream);
PrintWriter writer = new PrintWriter(outputStream);
while (true){
//1. 从控制台读取输入
System.out.print("->");
String request = scanner.next();
//2. 把请发送给服务器
writer.println(request);
writer.flush();
//3. 从服务器获取响应
if(!scannerNetwork.hasNext()){
break;
}
String reponse = scannerNetwork.next();
// 4. 把响应显示到控制台上
System.out.println(reponse);
}
}catch (IOException e){
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1",9090);
client.start();
}
}
4.1 服务器引入多线程
如果只是单个线程,无法同时响应多个客户端。此处给每个客户端都分配一个线程:
java
// 启动服务器
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
Socket clientSocket = serverSocket.accept();
Thread t = new Thread(() -> {
processConnection(clientSocket);
});
t.start();
}
}
4.2 服务器引入线程池
为了避免频繁创建销毁线程,也可以引入线程池:
java
// 启动服务器
public void start() throws IOException {
System.out.println("服务器启动!");
ExecutorService service = Executors.newCachedThreadPool();
while (true) {
Socket clientSocket = serverSocket.accept();
// 使用线程池,来解决上述问题
service.submit(new Runnable() {
@Override
public void run() {
processConnection(clientSocket);
}
});
}
}
长短连接
TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接:
- 短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能一次收发数据。
- 长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可以多次收发数据。
对比以上长短连接,两者区别如下:
- 建立连接、关闭连接的耗时:短连接每次请求、响应都需要建立连接,关闭连接;而长连接只需要第一次建立连接,之后的请求、响应都可以直接传输。相对来说建立连接,关闭连接也是要耗时的,长连接效率更高。
- 主动发送请求不同:短连接一般是客户端主动向服务端发送请求;而长连接可以是客户端主动发送请求,也可以是服务端主动发。
两者的使用场景有不同:
- 短连接适用于客户端请求频率不高的场景,如浏览网页等。
- 长连接适用于客户端与服务端通信频繁的场景,如聊天室,实时游戏等。
扩展了解:
基于BIO(同步阻塞IO)的长连接会一直占用系统资源。对于并发要求很高的服务端系统来说,这样的消耗是不能承受的。
由于每个连接都需要不停的阻塞等待接收数据,所以每个连接都会在一个线程中运行。
一次阻塞等待对应着一次请求、响应,不停处理也就是长连接的特性:一直不关闭连接,不停的处理请求。
实际应用时,服务端一般是基于NIO(即同步非阻塞IO)来实现长连接,性能可以极大的提升。
总结: Java 网络编程的核心是基于 Socket 套接字实现跨进程数据传输,核心概念包含发送 / 接收端、请求 / 响应、客户端 / 服务端。Socket 套接字主要分为 TCP 流套接字和 UDP 数据报套接字:TCP 具备有连接、可靠传输的特点,UDP 则无连接、传输高效但单次传输最大 64k。实操层面,UDP 通过 DatagramSocket 和 DatagramPacket 完成数据收发,TCP 依托 ServerSocket 和 Socket 实现通信,还可通过多线程、线程池优化多客户端并发处理。TCP 的长短连接适配不同场景,短连接适合低频请求,长连接适配高频通信,开发时需结合协议特性规避端口占用等问题,保障网络应用的稳定性与效率。