目录
[1. 为什么需要网络编程](#1. 为什么需要网络编程)
[2. 什么是网络编程](#2. 什么是网络编程)
[3. 网络编程的具体应用场景](#3. 网络编程的具体应用场景)
[3.1 常见的客户端服务端模型](#3.1 常见的客户端服务端模型)
[4. Socket套接字](#4. Socket套接字)
[4.1 概念](#4.1 概念)
[4.2 TCP与UDP的区别](#4.2 TCP与UDP的区别)
[4.2.1 有连接 vs 无连接](#4.2.1 有连接 vs 无连接)
[4.2.2 可靠传输 vs 不可靠传输](#4.2.2 可靠传输 vs 不可靠传输)
[4.2.3 面向字节流 vs 面向数据报](#4.2.3 面向字节流 vs 面向数据报)
[4.2.4 全双工 vs 半双工](#4.2.4 全双工 vs 半双工)
[4.3 UDP数据报套接字编程](#4.3 UDP数据报套接字编程)
[4.3.1 DatagramSocket](#4.3.1 DatagramSocket)
[4.3.2 DatagramPacket](#4.3.2 DatagramPacket)
[4.3.3 数据报套接字网络编程](#4.3.3 数据报套接字网络编程)
[4.3.3.1 UdpEchoServer](#4.3.3.1 UdpEchoServer)
[4.3.3.2 UdpEchoClient](#4.3.3.2 UdpEchoClient)
[4.3.3.3 客户端与服务器互信效果视频](#4.3.3.3 客户端与服务器互信效果视频)
[4.4 TCP流套接字编程](#4.4 TCP流套接字编程)
[4.4.1 ServerSocket](#4.4.1 ServerSocket)
[4.4.2 Socket](#4.4.2 Socket)
[4.4.3 流套接字网络编程](#4.4.3 流套接字网络编程)
[4.4.3.1 TcpEchoServer](#4.4.3.1 TcpEchoServer)
[4.4.3.2 TcpEchoClient](#4.4.3.2 TcpEchoClient)
[4.4.3.3 流套接字编程优化版](#4.4.3.3 流套接字编程优化版)
[4.4.3.4 最终版流套接字编程](#4.4.3.4 最终版流套接字编程)
通过上篇"初始网络原理",我们知道了主机间的网络数据传输,要经过从应用层------》物理层 的TCP/IP五层协议模型,这种概念。而网络数据传输是通过网络编程实现的,接下来本篇将梳理网络编程相关概念,并通过网络编程实现网络传输功能(也就是实现服务端------客户端 互信)
1. 为什么需要网络编程
网络编程是为了获取丰富的网络资源 ;打开浏览器一系列的数据信息涌入主机,打开腾讯视频,丰富的视频资源、图片资源、文本资源等,为你而来。
2. 什么是网络编程
网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络通信。****(小白们不用想了,你们是理解不了的,在看完这堆破概念以后,从Socket套接字开始深入思考,结合代码才能理解)
- 网络程序是指为实现网络通信而设计的、能在不同设备间传输数据的软件程序
基于Socket API开发的网络程序都是网络编程。但就算是同一台主机, 只要是不同进程(程序),不管是否连网,只要基于Socket进行数据传输也属于网络编程。
注意:不管是网络上的设备,还是同一台主机进行数据传输也好,发请求的和响应的进程都是不是同一个。
虽然在同一台主机上也可以实现网络编程,但要明确网络编程的目的是基于网络,给网络上的不同主机传输数据资源。
3. 网络编程的具体应用场景
网络编程广泛应用于服务端和客户端交互场景:

注意:谁发送请求谁就是客户端,谁处理请求返回响应谁就是服务端,响应不一定是发给发送请求的主机,响应也可以返回给其他主机。
3.1 常见的客户端服务端模型
- 客户端发送请求
- 服务端接收请求
- 服务端根据请求进行一系列计算处理,生成响应
- 返回响应
- 客户端解析响应,展示处理结果

上面的客户端和服务端模型,就像去饭店吃饭一样:你走进饭店说,老板来份辣椒炒肉(请求),老板说好嘞(接收请求),厨房就开始烹饪(处理业务),然后菜就端过来(返回响应),展现到你面前了。
思考: 网络编程是网络通信的基础。上图中,客户端与服务端互信时的请求、响应和处理业务的逻辑,都是程序员在++应用层(TCP/IP五层协议模型中的应用层)++ 自定义实现的,程序员想怎么定义,就怎么定义。咱Java程序员大多时候也都是在和应用层打交道,只调用传输层的api进行网络编程。而操作系统也提供了用于网络编程的api------》Socket
4. Socket套接字
4.1 概念
注意:计算机中的文件,通常指的是广义上的文件------》硬件也被抽象成文件方便管理。网卡抽象出来的文件,就是Socket文件,读写Socket,其实就是在读写网卡。
Socket 的中文专业术语叫**"套接字",这个专业术语就很奇怪;Socket的中文是插座,**但是不知道怎么回事就叫套接字了。
Socket封装了传输层的协议,封装的协议中就最主要的网络协议就是TCP和UDP,我们进行编程时只需要调用Socket,把数据传输给传输层,后面传输层到物理层的各种传输就不用管了,因为后面各层也已经封装好了(不需要关注后面,只调用使用即可)。
- Socket是操作系统提供给应用层调用的API,旨在方便进行网络编程。
- Socket是基于TCP/IP协议模型的网络通信的基本操作单元。
- 基于Socket的网络程序开发就是网络编程。
TCP、UDP风格差别很大,因此Socket也对应这提供了两套------》流套接字、数据报套接字
(Socket有多种,但主要的还是上述两种套接字)
流套接字基于TCP进行封装,数据报套接字基于UDP进行封装。
要想知道这俩套接字的区别,知道TCP与UDP协议的区别就行了。
4.2 TCP与UDP的区别
传输层两大核心协议:TCP、UDP,它们的风格差别很大。
TCP:有连接,可靠传输,面向字节流,全双工
UDP:无连接,不可靠传输,面向数据报,全双工
4.2.1 有连接 vs 无连接
协议有无连接,指的是在网络通信中,TCP保存了对端信息------》叫有连接;UDP协议本身不会保存对端的信息------》叫无连接。保存的主要信息就是那"五元组"。
连接指的是逻辑上的连接,我拥有你的IP、端口号信息才能传输数据给你。
奇思妙想:有人想问UDP协议不保存对端信息,怎么进行网络编程然后网络互信的?其实UDP本身不保存对端信息,但我们编程时,可以用变量手动记录对端信息。
4.2.2 可靠传输 vs 不可靠传输
可靠传输 :当传输的数据发生丢失(也就是丢包),TCP通过再次发送数据包提高传输成功率(可靠传输具体怎么实现的此处不讲,但一点也不影响后续编程),但成功率可不是100%,可靠传输不保证数据包能100%到达对端。
不可靠传输:发出数据包就不管了。
4.2.3 面向字节流 vs 面向数据报
面向字节流:读写数据,以字节为单位。
面向数据报:读写数据,以一个数据报为单位(不是字符,一个数据报的空间比字符大很多,读写能传输半个数据报)
4.2.4 全双工 vs 半双工
全双工:数据传输是双向的,两端可互相通信。
半双工:数据传输为单向,A能传给B,但B不能传给A。
4.3 UDP数据报套接字编程
Java基于原生UDP数据报套接字进行了封装,给出了两个类:
- DatagramSocket:是UDP Socket ,用于发送和接收数据报。
- DatagramPacket:是UDP Socket发送和接收的数据报
以下是网络编程接口的常用方法:
4.3.1 DatagramSocket
构造方法:
|--------------------------|-----------------------------------------------|
| 方法签名 | 方法说明 |
| DatagramSocket() | 创建一个UDP数据报套接字(Socket),绑定到本机一个随机端口上。(一般用在客户端) |
| DatagramSocket(int port) | 创建一个UDP数据报套接字(Socket),绑定到本机一个指定的端口上。(一般用在服务端) |
方法:
|---------------------------------|----------------------------|
| 方法签名 | 方法说明 |
| void receive(DatagramPacket dp) | 从此套接字接收数据报(没接收到数据报,方法就会阻塞) |
| void send(DatagramPacket dp) | 通过该方法发送数据报(不会阻塞,有就直接发送) |
| void close() | 关闭数据报套接字 |
new DatagramSocket对象,进程描述符表就创建了一个项,代表打开了一个文件(此处打开了Socket文件)。在Socket概念中讲过,Socket文件是网卡的代理,文件操作步骤也就3步,1.打开文件,2.读写文件,3.关闭文件
4.3.2 DatagramPacket
构造方法:
|--------------------------------------------------------------------------|-------------------------------------------------------------------------------------------|
| 方法签名 | 方法说明 |
| DatagramPacket(byte[] buf,int length) | 构造一个DatagramPacket用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数length) |
| DatagramPacket(byte[] buf,int offset,int length,SocketAddress address) | 构造一个DatagramPacket用来发送数据报,发送的数据在字节数组(第一个参数buf)中,从offset(偏移量)到指定长度length。address指定目的IP和端口号 |
方法:
|--------------------------|--------------------------------------------------------|
| 方法签名 | 方法说明 |
| InetAddress getAddress() | 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址(总之就是获取IP地址) |
| int getPort() | 从接收的数据报中,获取发送端主机端口号;或从发送的数据报中,获取接收端主机端口号(就是获取端口号) |
| byte[] getData() | 获取数据报中的数据 |
4.3.3 数据报套接字网络编程
4.3.3.1 UdpEchoServer
编程回显服务器(省略处理请求逻辑,请求发来什么,就响应什么,这就是回显):


或许有人注意到,代码中没有使用close()关闭套接字,疑问:这样不会导致文件描述符表耗尽出bug吗?
答:上面套接字不需要关闭(准确的说是不需要手动关闭);圆圈步骤2new 的UDP Socket(打开了Socket文件)被步骤1的成员变量引用着,while循环一直在接收处理不同客户端的请求,所以我们不需要Socket.close()手动关闭Socket文件,等到程序关闭,Socket文件会自动关闭。
java
package udpSocket;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UdpEchoServer {
private DatagramSocket socket = null;
public UdpEchoServer(int port) throws SocketException {
//指定一个端口号给服务器绑定使用
socket=new DatagramSocket(port);
}
//===================
//网络编程总结为3步:
//1.接收请求
//2.处理请求
//3.返回响应
//===================
//开启服务器
public void start() throws IOException {
//服务器一般是7*24小时开着的,遇到请求就处理,所以使用while死循环
while(true){
//构造接收数据报,字节数组为载荷部分,且数组为空(全为0)
DatagramPacket request = new DatagramPacket(new byte[4096],4096);
//使用上面的数据报接收请求,(receive()的参数为输出型参数)
//1.接收请求
socket.receive(request);
//为了好处理,转化成字符串(只将有效数据转换,也就是request.getLength())
String requestStr = new String(request.getData(),0, request.getLength());
//2.处理请求,生成响应
String responseStr = process(requestStr);
//构造响应数据报
DatagramPacket response = new DatagramPacket(responseStr.getBytes(),0,responseStr.getBytes().length,request.getSocketAddress());
//3.返回响应
socket.send(response);
//打印日志
System.out.printf("[%s:%d] req:%s resp:%s\n",request.getAddress().toString(),request.getPort(),requestStr,responseStr);
}
}
private String process(String requestStr) {
//因为是回显服务器,因此不做任何处理,直接请求数据
return requestStr;
}
public static void main(String[] args) throws IOException {
UdpEchoServer udpEchoServer = new UdpEchoServer(8080);
udpEchoServer.start();
}
}
4.3.3.2 UdpEchoClient
编程客户端
有了编写服务端(服务器)的经验,编写客户端就简单的多了:


java
package udpSocket;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket = null;
private String address;
private int port;
public UdpEchoClient(String address,int port) throws SocketException {
//构造参数不指定固定端口,系统会随机分配一个空闲端口。客户端一般不指定端口
socket = new DatagramSocket();
//记录目的IP和目的端口号
this.address=address;
this.port=port;
}
//===================
//1.用户输入内容
//2.发送请求
//3.接收响应
//4.解析响应数据
//===================
//开启客户端
public void start() throws IOException {
Scanner sc = new Scanner(System.in);
//客户端也得随时应对用户
while(true){
//1.用户输入数据
System.out.println("请输入内容:");
if (!sc.hasNext()){
return ;
}
String requestStr = sc.next();
//构造发送数据报
DatagramPacket request = new DatagramPacket(requestStr.getBytes(),0,requestStr.getBytes().length,InetAddress.getByName(address),port);
//2.发送数据
socket.send(request);
//3.接收响应
DatagramPacket response = new DatagramPacket(new byte[4096],4096);
socket.receive(response);
//4.解析响应
String processStr = new String(response.getData(),0,response.getLength());
String responseStr = process(processStr);
System.out.println(responseStr);
}
}
private String process(String process) {
return process;
}
public static void main(String[] args) throws IOException {
UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1",8080);
udpEchoClient.start();
}
}
4.3.3.3 客户端与服务器互信效果视频
UDP数据报套接字实现网络编程效果视频
额。视频上传后要收费,这里自己演示,不复杂的。。。
4.4 TCP流套接字编程
Java封装了TCP套接字,给出了两个类:
- ServerSocket 专门提供给服务端使用
- Socket 提供给服务端和客户端使用
TCP套接字没有receive()和send(),而是通过"输入输出流"读写Socket文件内容(读写网卡文件)
4.4.1 ServerSocket
构造方法:
|------------------------|-----------------------------|
| 方法签名 | 方法说明 |
| ServerSocket(int port) | 创建一个服务端流套接字Socket,并绑定到指定端口号 |
方法:
|-----------------|-----------------------------------------------------------------------|
| 方法签名 | 方法说明 |
| Socket accept() | 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待 |
| void close() | 关闭此套接字 |
4.4.2 Socket
Socket类可不是操作系统提供的那个Socket套接字。
构造方法:
|------------------------------|----------------------------------------|
| 方法签名 | 方法说明 |
| Socket(String host,int port) | 创建一个客户端流套接字Socket,并与对应IP的主机上的端口的进程建立连接 |
方法:
|--------------------------------|-------------|
| 方法签名 | 方法说明 |
| InetAddress getInetAddress() | 返回套接字所连接的地址 |
| InputStream getInputStream() | 返回此套接字的输入流 |
| OutPutStream getOutoutStream() | 返回此套接字的输出流 |
4.4.3 流套接字网络编程
4.4.3.1 TcpEchoServer


java
package tcpSocket;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class TcpEchoServer {
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
//不断与客户端建立连接
while(true){
//1.客户端发起连接,方法返回一个与客户端连接的Socket对象
Socket socket = serverSocket.accept();
//连接客户端,处理客户端发来的请求
processConnection(socket);
}
}
//===================
//2.接收请求
//3.解析请求,生成响应
//4.返回响应
// ===================
private void processConnection(Socket socket) {
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
//一次连接,客户端可能发送多次请求,使用while循环
while(true){
byte[] bytes = new byte[4096];
int n = inputStream.read(bytes);
//2.将二进制数据转换为字符串。n==-1代表已经读完请求数据
String requestStr = new String(bytes,0,n==-1?bytes.length:n);
//3.处理请求
String responseStr = process(requestStr);
//4.返回响应
//响应二进制数据
outputStream.write(responseStr.getBytes(),0,responseStr.getBytes().length);
System.out.printf("[%s,%d] req:%s resp:%s",socket.getInetAddress(),socket.getPort(),requestStr,responseStr);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private String process(String requestStr) {
return requestStr;
}
public static void main(String[] args) throws IOException {
TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);
tcpEchoServer.start();
}
}
4.4.3.2 TcpEchoClient


4.4.3.3 流套接字编程优化版
上述流套接字网络编程中,客户端与服务端,都用InputStream和OutputStream进行读写,而这两个IO流没有缓冲区,因此读写效率就会低不少。为了提高读写效率,用Scanner和PrintWriter分别对输入流和输出流套层壳:
服务端:

客户端:

4.4.3.4 最终版流套接字编程
学过多线程的,可能会意识到上述优化版服务端无法并发编程;若多个客户端同时发起连接,服务器只能建立一个连接,只有等第一个连接断开后,才能建立下一个客户端连接,如视频演示效果:
视频解析:最开始为了实现一个类多开,开启了"Allow....."。两个客户端同时连接,发送请求会出现,只有第一个客户端请求得到响应,其它客户端连接只能等前面的断开连接后才会连接上服务器
为了解决并发编程问题,使用线程池优化服务端连接代码:

最优版,服务端代码:(客户端无最优版)
java
package tcpSocket;
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 TcpEchoServer2 {
private ServerSocket serverSocket = null;
public TcpEchoServer2(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
//不断与客户端建立连接
while(true){
//1.客户端发起连接,方法返回一个与客户端连接的Socket对象
Socket socket = serverSocket.accept();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.submit(()->{
processConnection(socket);
});
}
}
//===================
//2.接收请求
//3.解析请求,生成响应
//4.返回响应
// ===================
private void processConnection(Socket socket) {
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
System.out.printf("[%s:%d] 客户端上线!\n", socket.getInetAddress(), socket.getPort());
//给inputStream套壳
Scanner scanner = new Scanner(inputStream);
//给outputStream套壳
PrintWriter printWriter = new PrintWriter(outputStream);
//一次连接,客户端可能发送多次请求,使用while循环
while(true){
if(!scanner.hasNext()){
System.out.printf("[%s:%d] 客户端下线\n",socket.getInetAddress(),socket.getPort());
break;
}
//2.接收请求
String requestStr = scanner.next();
//3.处理请求
String responseStr = process(requestStr);
//4.返回响应
//响应二进制数据
printWriter.println(responseStr);
//要使用flush()刷新缓冲区,把数据从缓冲区中刷出,给到客户端。否则客户端收不到数据
printWriter.flush();
//打印日志
System.out.printf("[%s,%d] req:%s resp:%s\n",socket.getInetAddress(),socket.getPort(),requestStr,responseStr);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private String process(String requestStr) {
return requestStr;
}
public static void main(String[] args) throws IOException {
TcpEchoServer2 tcpEchoServer2 = new TcpEchoServer2(9090);
tcpEchoServer2.start();
}
}
5. 总结
通过上述代码我们明白,使用操作系统提供的Socket API实现网络编程是非常简单的,但是上述代码创建的服务器只能自己访问,或被同一块区域网的设备访问(背后涉及到NAT)。
从TCP与UDP的四个特点对比和代码实现中,也就知道流套接字 和数据报套接字的区别(非常明显的是,一个Socket按字节传输,一个按数据报传输数据)。