网络编程套接字
文章目录
- 网络编程套接字
- 网络编程基础
- 什么是网络编程
- 网络编程中的基本概念
- Socket套接字
- Java数据报套接字模型
- Java流套接字模型
- UDP数据报套接字编程
-
- [API 介绍](#API 介绍)
-
- DatagramSocket
- [`DatagramSocket` 构造方法](#
DatagramSocket
构造方法) - [`DatagramSocket` 方法](#
DatagramSocket
方法) - DatagramPacket
- [`DatagramPacket` 构造方法](#
DatagramPacket
构造方法) - [`DatagramPacket` 方法](#
DatagramPacket
方法) - InetSocketAddress
- TCP流套接字编程
-
- [API 介绍](#API 介绍)
-
- ServerSocket
- [`ServerSocket` 构造方法:](#
ServerSocket
构造方法:) - [`ServerSocket` 方法:](#
ServerSocket
方法:) - Socket
- [`Socket` 构造方法:](#
Socket
构造方法:) - [`Socket` 方法:](#
Socket
方法:)
- 服务器引入多线程
- 服务器引入线程池
- 长短连接
网络编程基础
为什么需要网络编程?
- 主要是因为网络上,有丰富的资源
- 所谓的网络资源,其实就是在网络中可以获取各种数据资源
- 因为需要网络来传输,所以就有了网络编程
什么是网络编程
- 网络编程,是指网络上的主机,通过不同的进程,以编程的形式实现网络通信(或者称为网络数据传输)

- 当然我们只需要满足不同的进程就行,即便是同一个主机,只要是不同的进程,基于网络来传输数据,也属于网络编程
- 一般开发来说,在条件有限的情况下,一般也是在同一个主机中运行多个进程,来完成网络编程
- 但是我们一定要明确,我们目的是提供网络上不同主机,基于网络编程来传输数据:
- 进程A:编程来获取资源
- 进程B:编程来提供资源
网络编程中的基本概念
发送端和接收端
- 发送端:数据的发送方进程,称为发送端,发送端主机即为源主机
- 接收端:数据的接收方进程,称为接收端,接收端主机即为目的主机
- 收发端:发送和接收端两端,即称为收发端
注意⚠️:
发送端和接收端是相对的,只是一次网络数据传输后产生数据流向的概念
请求和响应
- 一般来说,获取一个网络资源,涉及到两次网络数据传输
- 第一次:请求数据发起
- 第二次:响应数据发起

客户端和服务端
- 服务端:在常见的网络数据传输场景下,把提供服务的一方进程,称为服务端
- 客户端:获取服务的一个进程,称为客户端
对于服务来说,一般是提供:
- 客户端获取服务端数据:

- 客户端资源保存在服务端:

常见的客户端服务端模型
最常见的场景,客户端是指给用户使用的程序,服务器是提供用户服务的程序:
- 客户端先发送请求到服务端
- 服务端根据请求数据,执行相应的业务处理结果
- 服务端返回响应:发送业务处理结果
- 客户端再根据响应数据,展示处理结果(展示获取的资源,或者提示保存资源的处理结果)

Socket套接字
概念
- Socket套接字,是由系统提供用于网络通信技术的API,是基于TCP/IP协议的网络通信的基本操作单位。基于Socket套接字的网络程序开发就是网络编程
分类
Socket套接字主要针对传输层协议划分为以下三类:
- TCP协议
有以下特点:
- 有连接
- 可靠传输
- 面向字节流
- 全双工
- 有接收缓冲区,也有发送缓冲区
- 大小不限
注意⚠️:
对于字节流来说,可以简单地理解为,传输数据是基于IO流没有关闭的时候,是无边界数据,可以多次发送,也可以分开多次接收
- UDP协议
有以下特点:
- 无连接
- 不可靠传输
- 面向数据报
- 全双工
- 只有接收缓冲区,没有发送缓冲区
- 大小受限,一次最多传输64K
注意⚠️:
对于数据报来说,可以简单地理解为,传输数据是一块一块的,发送一块数据假如是100个字节,必须一起发送,不能分开发送。接收也必须接收100个字节,不能分开接收
概念解释
- 连接:建立通信的双方,互留信息(TCP需要双方同意才能建立通信,UDP通信直接发送数据,不需要对方同意,不保留信息)
- 传输:假如A向B发送信息,发送失败会采取对应措施(例如:重传),这就是可靠传输。假如不采取对应措施,就是不可靠传输
- 全双工:允许双向通信
注意⚠️:
可靠传输采取对应的措施,可以补救。但是机制更加复杂,传输效率会降低
Java数据报套接字模型
- 对于UDP协议来说,具有无连接,面向数据报的特点,就是每次都是没有建立连接,并且一次传输和接收所有数据
- Java中使用UDP协议通信,主要是基于DatagramSocket类来创建数据报的套接字,并使用DatagramPacket作为发送或者接收UDP数据报。
流程如下:
- 以上只是一次发送端的UDP数据报发送,以及接收端的数据报接收,并没有返回数据,也就只有请求,没有响应。但对于一个服务器来说,重要的是提供多个客户端的请求处理以及响应,流程如下:

Java流套接字模型
- Socket通信模型

- Socket编程注意事项

- 客户端和服务器:开发时,经常是基于一个主机开启两个线程作为客户端和服务器,但真实的场景,一般是不同的主机
- 注意目的IP和目的端口号,标识了一次数据传输时要发送数据终点的主机和端口号
- Socket编程我们是使用流套接字和数据报套接字,基于传输层的TCP或UDP协议,但是应用层协议,我们也是需要考虑的,这块后面我们在说明如何设计应用层协议再说
- 关于端口被占用问题
- 如果进程A已经绑定了一个端口号,再启动一个进程B绑定该端口号,就会报错,这种就是端口被占用。对于Java来说,端口被占用的常见报错信息如下:

解决端口被占用的问题:
- 如果占用端口的进程A不需要运行,就可以关闭A后,再启动需要绑定该端口的进程B
- 如果需要运行A进程,则可以修改进程B的绑定端口,换为其他没有使用的端口。
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 地址和端口号 |
java
客户端:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Scanner;
public class UdpEchoServerClient {
private DatagramSocket socket = null;
private String serverIp = "";
private int serverPort = 0;
public UdpEchoServerClient(String ip, int port) throws SocketException {
socket = new DatagramSocket();
serverIp = ip;
serverPort = port;
}
public void start() throws IOException {
System.out.println("客户端启动");
Scanner scanner = new Scanner(System.in);
while (true) {
String request = scanner.nextLine();
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length, InetAddress.getByName(serverIp), serverPort);
socket.send(requestPacket);
String response = new String(requestPacket.getData(), 0, request.getBytes().length);
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoServerClient server = new UdpEchoServerClient("123.0.0.1", 9090);
server.start();
}
}
服务器:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
//TIP 要<b>运行</b>代码,请按 <shortcut actionId="Run"/> 或
// 点击装订区域中的 <icon src="AllIcons.Actions.Execute"/> 图标。
public class UdpEchoServer {
private DatagramSocket socket = null;
public void UdpEchoSever(int port) throws SocketException{
socket = new DatagramSocket(port);
}
public void start() throws IOException{
System.out.println("服务器启动");
while(true){
DatagramPacket requesPacket = new DatagramPacket(new byte[1024], 1024);
socket.receive(requesPacket);
String request = new String(requesPacket.getData(), 0, requesPacket.getLength());
String response = process(request);
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length, requesPacket.getAddress(), requesPacket.getPort());
socket.send(responsePacket);
System.out.println();
}
}
public String process(String request){
return request;
}
public static void main(String[] args) {
try{
UdpEchoServer server = new UdpEchoServer();
server.start();
}
catch (IOException e){
e.printStackTrace();
}
}
}
TCP流套接字编程
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() |
返回此套接字的输出流 |
java
客户端:
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(int port) throws IOException {
socket = new Socket(serverIp, severPort);
}
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
System.out.println("Server started on port 8080");
try(InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()) {
PrintWriter printWriter = new PrintWriter(outputStream);
Scanner console = new Scanner(inputStream);
while(true){
String line = console.next();
printWriter.println(line);
System.out.println(line);
printWriter.flush();
String response = console.nextLine();
System.out.println(response);
}
}catch(IOException e){
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient tcpEchoClient = new TcpEchoClient("",8080);
tcpEchoClient.start();
}
}
服务器:
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TcpEchoServer {
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("Server started");
ExecutorService executorService = Executors.newCachedThreadPool();
while (true) {
Socket clientSocket = serverSocket.accept();
/*Thread thread = new Thread(() -> {
processConnection(clientSocket);
});
thread.start();*/
executorService.submit((new Runnable() {
@Override
public void run() {
processConnection(clientSocket);
}
});
}
}
private void processConnection(Socket clientSocket) {
System.out.println("[%s:%d] 客户端上线了\n", clientSocket.getInetAddress(), clientSocket.getPort());
try(InputStream inputStream = clientSocket.getInputStream();OutputStream outputStream = clientSocket.getOutputStream()){
while(true){
Scanner scanner = new Scanner(inputStream);
if(!scanner.hasNextLine()){
break;
}
String request = scanner.nextLine();
String response = process(request);
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response);
printWriter.flush();
System.out.printf("[%s : %d request = %s, response = %s\n", clientSocket.getInetAddress(), clientSocket.getPort(), request, response);
}
}catch (IOException e){
e.printStackTrace();
}finally{
try{
clientSocket.close();
}catch(IOException e){
e.printStackTrace();
}
}
}
private String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(8080);
server.start();
}
}
服务器引入多线程
- 如果是单线程没办法,同时处理多个客户端
- 此处给每个客户端都分配一个线程
java
// 启动服务器
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
Socket clientSocket = serverSocket.accept();
Thread t = new Thread(() -> {
processConnection(clientSocket);
});
t.start();
}
}
服务器引入线程池
- 避免频繁的创建和销毁线程,也可以引入线程池
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)来实现长连接,性能可以极大的提升