目录
1.Socket套接字
操作系统给应用程序(传输层给应用层)提供的API起了个名字就叫socket api
socket本身含义是插槽,类似于电脑主板上的插槽接口
Java中提供了两套API,UDP一套,TCP一套
1.1TCP和UDP的区别
TCP有连接,可靠传输,面向字节流,全双工
UDP无连接,不可靠传输,面向数据报,全双工
- 有连接/无连接
此处谈到的为抽象的连接,通信双方如果保存了通信对端的信息(IP+端口),就相当于是"有连接",如果不保存对端信息就是"无连接"- 可靠传输/不可靠传输
此处的可靠指的是“尽可能”,不可靠就是完全不考虑数据是否能够到达对方
TCP内置了一些机制,能够保证可靠传输
①感知对方是不是收到了
②重传机制,在对方没收到时进行重试
UDP则没有可靠性机制,完全不管发出去的数据是否顺利到达对方
可靠传输更为复杂,TCP协议的设计就要比UDP复杂很多,也会损失一些传输数据的效率- 面向字节流/数据报
TCP是面向字节流的,TCP的传输过程就和文件流/水流是一样的特点.
UDP面向数据报
传输数据的基本单位,不是字节了,而是"UDP数据报"
一次发送/接受,必须发送/接受完整的UDP数据报- 全双工/半双工
全双工:一个通信链路,可以发送数据也可以接收数据(双向通信)
半双工:一个通信链路,只能发送/只能接收(单向通信)
2.UDP api的使用
通过代码直接操作网卡,不好操作(网卡有很多种不同的型号,之间提供的api都会有差别)
操作系统就把网卡概念封装成socket.应用程序员不必关注硬件的差异和细节,统一去操作socket对象,就能间接的操作网卡.
socket就像遥控器一样,从socket里读数据就相当于接受网卡传来的网络信号(数据流向:网卡 → 内核→ Socket),
往socket里写数据相当于控制网卡发送网络信号(数据流向:Socket → 内核 → 网卡)
Socket 与遥控器的类比逻辑 类比对象 遥控器 Socket 核心功能 远程控制设备(如电视、空调) 远程控制网络数据收发 操作抽象 按下按钮(抽象操作)→ 遥控器内部电路处理 → 红外信号发射 调用 Socket 接口(如 send()
/recv()
)→ 操作系统内核处理 → 网卡硬件收发数据用户感知 无需关心红外信号的物理特性 无需关心网卡驱动、TCP/IP 协议细节
2.1DatagramSocket
DatagramSocket表示一个socket对象,操作系统的概念socket就可以认为是操作系统中,广义的文件下,里面的一种文件类型就是网卡这种硬件设备抽象的表示形式
DatagramSocket构造方法 DatagramSocket() 创建一个UDP数据报套接字的Socket,绑定到本机任意⼀个随机端口(一般用于客户端) DatagramSocket(int port) 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务端)
DatagramSocket方法 void receive(DatagramPacket p)
接收数据时使用
从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) void send(DatagramPacket p)
发送数据时使用从此套接字发送数据报包(不会阻塞等待,直接发
送)void close() 关闭此数据报套接字 2.2DatagramPacket
- DatagramPacket代表一个UDP数据报
DatagramPacket构造方法 DatagramPacket(byte[] buf, int length) 构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数length) DatagramPacket(byte[] buf, int offset, int length,SocketAddress address) 构造一个DatagramPacket以用来发送数据报,发送的SocketAddress address)数据为字节数组(第一个参数buf)中,从0到指定长度(第二个参数length)。address指定目的主机的IP和端口号
DatagramPacket方法 InetAddress getAddress() 从接收的数据报中,获取发送端主机IP地址;或从发
送的数据报中,获取接收端主机IP地址int getPort() 从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号 byte[] getData() 获取数据报中的数据
3.UDP数据报套接字编程
写一个最简单的客户端服务器程序,不涉及到业务流程只是对于api的用法做演示
回显服务器"(echo server)
客户端发啥样的请求,服务器就返回啥样的响应
没有任何业务逻辑,进行任何计算或者处理
3.1UdpEchoServer服务器
DatagramSocket表示一个socket对象
DatagramPacket进行UDP传输时的一个基本单位- 此处代码中,socket生命周期,应该是跟随整个进程的
进程结束了,socket才需要关闭
此时,就算代码中没有close,进程关闭,也就会释放文件描述符表里的所有内容,也就是相当于close了
- 调用这个构造方法的过程中,jvm就会调用系统的socket api完成端口号和进程之间的关联动作
对于一个系统来说,同一时刻,一个端口号,只能被一个进程绑定
但是一个进程可以绑定多个端口号(通过创建多个socket对象来完成)- 对于服务器来说,主要的工作,就是不停的处理客户端发来的请求,由于客户端啥时候来请求,服务器也无法预测,服务器只能时刻准备好,随时有客户端来了就随时立即处理
- receive是通过"输出型参数"获取到网卡上收到的数据
receive是从网卡上读取数据,但是调用receive的时候,网卡上可不一定就有数据
如果网卡上收到数据了,receive立即返回,获取到收到的数据
如果网卡上没有收到数据,receive就会阻塞等待,一直等待到真正收到数据为止.
- DatagramPacket自身需要存储数据,存储空间大小由外部定义
- DatagramSocket这个对象中,不持有对方(客户端)的ip和端口的.
进行send的时候,就需要在send的数据包里,把要发给谁这样的信息,写进去,才能够正确的把数据进行返回
- 请求(request):客户端主动给服务器发起的数据
响应(response):服务器给客户端返回的数据
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);
}
public String process (String request){
return request;
}
//通过start启动服务器的核心流程
public void start() throws IOException {
System.out.println("服务器启动");
while(true) {
//通过死循环不停处理客户端请求
//1.读取客户端的请求并解析
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
socket.receive(requestPacket);
//上述收到的数据是二进制byte[]的形式体现的,后续若需打印需转换为字符串
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);
//4.打印日志
System.out.printf("[%s:%d] req=%s,resp=%s\n", requestPacket.getAddress(),requestPacket.getPort(),
request,response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}
3.2UdpEchoClient客户端
- 客户端这边创建socket最好不要指定端口号,客户端是主动的一方,不需要服务器来找他,客户端就不需要指定端口号了(不指定不代表没有,客户端这边自动分配了一个端口)
因为客户端是在用户的电脑上运行的,所以我们并不知道电脑上都有哪些程序都已经占用了哪些端口,因此让系统自动分配就能确保分配的是无人使用的空闲端口
服务器程序运行在服务器主机上,服务器指定固定端口就不怕程序冲突
- 客户端与服务器进行通信时要保存服务器的地址和端口
- 构造数据报并发送请求给服务器,指定服务器的IP地址和端口
- 127.0.0.1这个是特殊的IP,环回IP,这个IP就代表本机,如果客户端和服务器在同一个主机上,就使用这个IP
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIP;
private int serverPort;
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("启动客户端");
Scanner scanner = new Scanner(System.in);
while(true) {
//1.从控制台读取到用户的输入
System.out.println("->");
String request = scanner.next();
//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[4096],4096);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(),0,responsePacket.getLength());
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
client.start();
}
}
3.3客户端和服务器相互配合的完整流程
- 服务器读取客户端的请求并解析,receive阻塞,等待服务器收到请求
客户端构造UDP请求发送给服务器,发送完毕的同时客户端继续往下走,在receive处阻塞,等待服务器的响应
服务器收到了请求,从receive返回,继续往下走,将收到的数据转换成字符串,因为是回显服务器所以响应就是请求,把响应写回到客户端
客户端收到了服务器返回的响应之后就会从上面的receive解除阻塞继续执行,将响应转为字符串之后进行打印
服务器send完毕后,打印日志,然后进入下一轮循环,继续在receive处进行阻塞 客户端打印完毕这里之后,也要进入下一次循环,下一次循环,就要继续从scanner中读取
用户输入的内容了.
- 服务器无连接
4.TCP api的使用
4.1ServerSocket
ServerSocket 是创建TCP服务端Socket的API,专门给服务器使用的socket对象
ServerSocket构造方法 方法签名 方法说明 ServerSocket(int port) 创建一个服务端流套接字Socket,并绑定到指定端口
ServerSocket方法 方法签名 方法说明 Socket accept() 开始监听指定端口(创建时绑定的端口),有客户端
连接后,返回一个服务端Socket对象,并基于该
Socket建立与客户端的连接,否则阻塞等待void close() 关闭此套接字 TCP是有连接的,有连接就需要有一个“建立连接”的过程
建立连接的过程就类似于打电话,此处的accept就相当于"接电话"由于客户端是"主动发起”的一方,服务器是"被动接受”的一方,所以客户端打电话,服务器接电话
4.2Socket
Socket是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回服务端的Socket。
不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。
即会给客户端使用也会给服务器使用
Socket构造方法 方法签名 方法说明 Socket(String host, int port) 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接
构造这个对象,就是和服务器“打电话”“建立连接”
Socket方法 方法签名 方法说明 InetAddress getInetAddress() 返回套接字所连接的地址 InputStream getInputStream()
返回此套接字的输入流 OutputStream getOutputStream() 返回此套接字的输出流
5.TCP数据报套接字编程
同一个协议下,一个端口只能被一个进程绑定,9090端口在UDP下被一个进程绑定了,还可以在TCP下被另一个进程绑定
不能绑定多个UDP或TCP如图表示绑定的端口号已被占用,此时就需要找找,端口是被谁绑定了,找到对应的进程
决定是结束旧的进程,还是修改新进程的端口
5.1TcpEchoServer服务器
- TCP是全双工通信,所以既可以读也可以写,inputStream outputStream
- inputStream.read处理的数据类型是二进制字节,scanner处理的是文本字符串更方便读取
- 写入响应的时候末尾加上\n,TCP是字节流的.读写方式存在无数种可能,就需要有办法区分出,从哪里到哪里是一个完整的请求数据,此处就可以引入分隔符来区分
读取数据时就隐藏了条件,请求应以空白符(空格,回车,制表符等)结尾
因此此处就约定,使用\n作为请求和响应的结尾标志
后续客户端,也会使用scanner.next读取响应- 线程池的创建是为了复用线程,解决了客户端发一个请求之后就快速断开连接了
ExecutorService service = Executors.newCachedThreadPool();
客户端持续的发送请求处理响应,连接会保持很久~~
服务器的两处阻塞
- 等待客户端连上
//客户端 socket = new Socket(serverIP,port); //服务器 Socket clientSocket = serverSocket.accept();
- 等待客户端发送数据
//客户端,把请求发送给服务器 printWriter.println(request); //服务器 if(!scanner.hasNext()) { //如果scanner无法读取数据,说明客户端关闭了连接,导致服务器这边读取到了“末尾” break; }
容易出现的三个bug
- 服务器发送了数据之后,并没有任何响应,此处的情况是客户端并没有真正把请求发送出去
printWriter.println(request);
printWriter这个类“自带缓冲区”,把请求先放到内存的缓冲区里,由于此处数据比较少,因此这样的数据就一直停留在缓冲区中无法进行发送
引入缓冲区之后,进行写入数据的操作,不会立即触发1O,而是先放到内存缓冲
区中,等到缓冲区里攒了一波之后,再统一进行发送
因此我们需要引入flush操作“刷新缓冲区”
printWriter.flush();
针对cilentSocket没有进行close操作
像ServerSocket,DatagramSocket他们的生命周期都是跟随整个进程的,而此处的此cilentSocket是“连接级别”的数据,随着客户端断开连接了,这个socket也就不再使用了,即使是同一个客户端断开之后重新连接socket也是不同的,因此这样的socket需要主动关闭,否则就会造成文件资源泄露
clientSocket.close();
只能为一个客户端提供服务,应该满足同时给多个客户端提供服务
当第一个客户端连上服务器之后,服务器代码就会进入processConnect内部的while循环,此时第二个客户端尝试连接时,无法执行到第二次accept,所以第二个客户端发来的请求数据都积压在操作系统的接收缓冲区中
此处无法处理多个客户端,本质上是服务器代码结构存在问题,采取了双重while循环的写法.就会导致,进入里层while的时候,外层while无法继续执行了,此时我们只需要改为一层循环分别进行执行即可,用多线程来实现,
主线程是accept,每个客户端连接由独立线程处理,主线程继续接收下一个连接。public void start() throws IOException { System.out.println("启动服务器"); //System.err.println("启动服务器"); //线程池 ExecutorService service = Executors.newCachedThreadPool(); while(true) { Socket clientSocket = serverSocket.accept(); service.submit(() -> { try { processConnection(clientSocket); } catch (IOException e) { throw new RuntimeException(e); } }); } }
import java.io.*;
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("启动服务器");
System.err.println("启动服务器");
//线程池
ExecutorService service = Executors.newCachedThreadPool();
while(true) {
Socket clientSocket = serverSocket.accept();
service.submit(() -> {
try {
processConnection(clientSocket);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
}
//针对一个连接提供处理逻辑
private void processConnection(Socket clientSocket) throws IOException {
//先打印一下客户端的信息
System.out.printf("[%s:%d] 客户端上线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
//获取到 socket 中持有的流对象
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
//使用scanner包装一下inputStream就可以更方便的读取这里的请求数据了
Scanner scanner = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
while(true) {
//1.读取请求并解析
if(!scanner.hasNext()) {
//如果scanner无法读取数据,说明客户端关闭了连接,导致服务器这边读取到了“末尾”
break;
}
String request = scanner.next();
//2.根据请求计算响应
String response = process(request);
//3.把响应写回给客户端
//此处可以按照字节数组直接来写,也可以有另外一种写法
//outputStream.write(response.getBytes());
printWriter.println(response);
printWriter.flush();
//4.打印日志
System.out.printf("[%s:%d] req=%s; resp=%s\n", clientSocket.getInetAddress(), clientSocket.getPort(),
request, response);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
clientSocket.close();
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
5.2TcpEchoClient客户端
对象一new好就会和服务器建好连接,服务器accept如果建立连接失败就会在构造对象时抛出异常
public TcpEchoClient(String serverIp, int serverPort) throws IOException { socket = new Socket(serverIp, serverPort); }
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(String serverIp, int serverPort) throws IOException {
socket = new Socket(serverIp, serverPort);
}
public void start() {
System.out.println("客户端启动");
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner scanner = new Scanner(inputStream);
Scanner scannerIn = new Scanner(System.in);
PrintWriter printWriter = new PrintWriter(outputStream);
while (true) {
// 1. 从控制台读取数据
System.out.print("-> ");
String request = scannerIn.next();
// 2. 把请求发送给服务器
printWriter.println(request);
printWriter.flush();
// 3. 从服务器读取响应
if (!scanner.hasNext()) {
break;
}
String response = scanner.next();
// 4. 打印响应结果
System.out.println(response);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
client.start();
}
}