文章目录
- Socket套接字
- TCP和UDP的区别
- 有连接/无连接
- 可靠传输/不可靠传输
- 面向字节流/面向数据报
- 全双工/半双工
- UDP/TCP api的使用
- UDP
- DatagramSocket
- DatagramPacket
- InetSocketAddress
- 练习
- TCP
- ServerSocket
- Socket
- 练习
Socket套接字
Socket是计算机网络中的一种通信机制,通过在应用程序之间建立网络连接,实现数据的传输和交流。它提供了一组接口和协议,使得网络上的不同设备或进程能够进行通信。
换句话说,就是操作系统给应用程序(传输层给应用层)提供的API起了个名字,就叫做Socket.
我们通过代码来直接操作网卡,不太好操作.
因为网卡有很多种型号,之间提供的api都会有差别.
为了方便程序员操作网卡,操作系统就把网卡这个概念给封装成Socket,应用程序员就不必关注硬件的差异和细节,只需要操作Socket对象就能间接的操作网卡.
就跟遥控器差不多~
需要注意的是,接下来要说的都是操作系统提供的Java版本的Socket api.不是系统原生的api,而是JDK封装好的!
Socket api提供了两组不同的api
- UDP有一套
- TCP也有一套
TCP和UDP的区别
TCP: 有连接,可靠传输,面向字节流,全双工.
UDP: 无连接,不可靠传输,面向数据包,全双工.
有连接/无连接
此处谈到的连接,是"抽象"的连接.
通信双方,如果保存了通信对端的信息,就相当于是"有连接",如果不保存对端的信息,就是"无连接".
举个例子,比方说结婚.
结婚证,一式两份,本子上写着新郎和新娘两个人的名字/照片等信息.
一份由新郎保存.
一份由新娘保存.
此时结婚的这两个人就相当于建立了"抽象的/逻辑上的"连接.
可靠传输/不可靠传输
此处谈到的"可靠",不是指100%能到达对方,而是"尽可能".
因为网络环境非常复杂,存在很多不确定的因素~
再厉害的技术,也顶不过挖掘机一铲子~
相对来说"不可靠",就是指完全不考虑数据是否能到达对方.
TCP内置了一些机制,能够保证可靠传输
- 感知到对方是不是收到了
- 重传机制,在对方没收到的时候进行重试
UDP则没有可靠性机制,完全不管发出去的数据是否能够到达对方.
面向字节流/面向数据报
TCP是面向字节流的,TCP的传输过程就和文件流/水流是一样的特点.
UDP是面向数据报的.
此时,传输数据的基本单位就不是字节了,而是"UDP数据报".
一次发送/接收,必须发送/接收完整的UDP数据报.
全双工/半双工
全双工: 一个通信链路,可以发送数据,也可以接收数据.(双向通信)
半双工: 一个通信链路,只能发送/接收(单向通信).
UDP/TCP api的使用
UDP
DatagramSocket
DatagramSocket是UDP Socket,用于发送和接收UDP数据报.
DatagramSocket构造方法:
DatagramSocket方法:
DatagramPacket
DatagramPacket是UDP Socket发送和接收的数据报.
DatagramPacket构造方法:
DatagramPacket方法:
构造UDP发送的数据报时,需要传入SocketAddress,该对象可以使用InetSocketAddress来创建.
InetSocketAddress
InetSocketAddress (SocketAddress的子类)构造方法:
练习
写一个最简单的客户端服务器程序,“回显服务器”(echo server).客户端发啥样的请求,服务器就返回啥样的响应.
服务器代码
package javaEE.Internet;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class A {
private DatagramSocket socket = null;
// SocketException 这个异常是IOException的子类
public A(int port) throws SocketException {
// 对于服务器这一端来说,需要在Socket对象创建的时候,就指定一个端口号,作为构造方法的参数
// 后续服务器开始运行之后,操作系统就会把端口号和该进程关联起来.
// 创建Socket一定要指定端口号,服务器必须是指定了端口号,客户端主动发起请求的时候,才能找到服务器
socket = new DatagramSocket(port);
// 在调用这个构造方法的过程中,jvm就会调用系统的Socket api来完成"端口号-进程"之间的关联动作
// 对于一个系统来说,同一时刻,一个端口号,只能被一个进程绑定
// 但是一个进程可以绑定多个端口号(通过多个Socket对象来完成)
}
// 通过start来启动服务器的核心流程
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
// 通过死循环来不停的处理客户端的请求
// 1. 读取客户端的请求并解析
// DatagramPacket自身需要存储数据,但是存储数据的空间具体多大,需要外部来定义
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
// receive是从网卡上读取的数据,但是调用receive的时候,网卡上可不一定就有数据.
// 如果网卡上收到数据了,receive立即返回获取到的数据,如果网卡上没有收到数据,receive就会阻塞等待,一直等待到真正收到数据为止.
socket.receive(requestPacket);
// 上述收到的数据是二进制byte[]的形式体现的,后续代码如果要进行打印之类的处理操作
// 那就需要转换成字符串才好处理
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
// 2. 根据请求计算响应,由于此处是回显服务器,响应就是请求.
String response = process(request);
// 3. 把响应写回到客户端
// response.getBytes().length 这里容易写错,容易写成response.length().
// response.getBytes().length获取字节数组,得到字节数组的长度,单位是"字节".
// response.length()获取字符串中"字符"的个数,单位是"字符".
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
requestPacket.getSocketAddress());
// UDP有一个特点,无连接.
// 所谓的连接,就是通信双方,保存对方的信息(IP+端口)
// DatagramPacket 这个对象中,不持有对方(客户端)的ip和端口.
// 在进行send的时候,就需要在send的数据包里,把要发给谁这样的信息写进去,才能够正确的把数据返回.
socket.send(responsePacket);
// 4. 把日志打印一下
System.out.printf("[%s:%d] req=%s, resp=%s\n", requestPacket.getAddress(), requestPacket.getPort(),
request, response);
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
A a = new A(9090);
a.start();
}
}
客户端代码
package javaEE.Internet;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class B {
private DatagramSocket socket = null;
private String serverIP;
private int severPort;
public B(String serverIP, int severPort) throws SocketException {
// 客户端这边,创建Socket,最好不要指定端口号
// 客户端是主动的一方,不需要让服务器来找他
// 客户端就不需要指定端口号了(不指定不代表没有,客户端这边的端口号是系统自动分配了一个端口)
// 这里还有一个重要的原因,如果客户端这里指定了端口号之后,由于客户端是在用户的电脑上运行的,天知道用户的电脑上都有哪些程序,都已经占用了哪些端口号了
socket = new DatagramSocket();
this.serverIP = serverIP;
this.severPort = severPort;
}
public void start() throws IOException {
System.out.println("启动客户端!");
Scanner scanner = new Scanner(System.in);
while (true) {
// 1. 从控制台读取到用户的输入
System.out.print("-> ");
String request = scanner.next();
// 2. 构造出一个UDP请求,发送给服务器
// 此处是给服务器发送数据,发送数据的时候UDP数据报里就需要带有目标的IP和端口
// 接收数据的时候,构造出的UDP数据报,就是一个空的数据报.
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName(this.serverIP),this.severPort);
socket.send(requestPacket);
// 3. 从服务器读取到响应
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
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 {
B b = new B("127.0.0.1",9090);
// "127.0.0.1"这个是特殊的IP,环回IP
// 这个IP就代表本机,如果客户端和服务器在同一个主机上,就使用这个IP
b.start();
}
}
运行结果:
在以上代码的基础上,实现一个"英译汉"的效果.
"英译汉"服务器代码:
package javaEE.Internet;
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
// 实现一个"英译汉"
public class C extends A{
private Map<String,String> dict = new HashMap<>();
@Override
public String process(String request) {
return dict.getOrDefault(request,"未知单词");
}
public C(int port) throws SocketException {
super(port);
dict.put("cat","小猫");
dict.put("dog","小狗");
dict.put("pig","小猪");
dict.put("sheep","小羊");
// 真实的服务器,需要很多的单词,可能是上万个.
}
public static void main(String[] args) throws IOException {
C server = new C(9090);
server.start();
}
}
客户端代码不需要改动~
运行效果:
TCP
ServerSocket
ServerSocket是创建TCP服务端Socket的API.
ServerSocket构造方法:
同一个协议下,一个端口号只能被一个进程绑定.
比如,9090端口,在UDP下被一个进程绑定了;9090这个端口,还可以在TCP下被另一个进程绑定.
ServerSocket方法:
Socket
Socket是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket.
不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的.
Socket构造方法:
Socket方法:
InputStream和OutputStream称为"字节流".
前面针对文件操作的方法,针对此处的tcp socket来说,也是完全适用的.
这里为啥没有类,来表示一个"TCP数据报"呢?
TCP是面向字节流的,TCP上传输数据的基本单位就是byte.
UDP是面向数据报,UDP这里需要定义专门的类,来表示UDP数据报,作为UDP传输的基本单位
练习
编写TCP回显服务器.
服务器代码:
package javaEE.Internet;
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;
public class D {
private ServerSocket serverSocket = null;
public D(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("启动服务器");
while (true) {
// 像ServerSocket,DatagramSocket,它们的生命周期,都是跟随整个进程的
// 这里的clientSocket是"连接级别"的数据
// 随着客户端断开连接,这个socket也就不再使用了.(即使是同一个客户端,断开之后,重新连接,也是一个新socket,和旧socket不是同一个了)
// 因此,这样的socket就应该主动关闭掉,避免出现文件资源泄漏的问题.
Socket clientSocket = serverSocket.accept();
Thread t = new Thread(() -> {
try {
processConnection(clientSocket);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
t.start();
}
}
// 针对一个连接,提供处理逻辑
private void processConnection(Socket clientSocket) throws IOException {
// 先打印一下客户端的信息
System.out.printf("[%s,%d] 客户端上线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
// 获取到socket中持有的流对象.
// TCP是全双工通信的通信,一个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);可以让我们在写入的时候加上 \n
// 由于上面if语句里写了!scanner.hasNext(),这就意味着请求应该是以"空白符"(空格,回车,制表符,垂直制表符,翻页符...)结尾的.
// 因此此处就约定,使用\n来作为请求和响应的结尾标志.
// 后续客户端,也会使用scanner.next来读取响应.
printWriter.println(response);
// 由于printWriter这样的类,以及很多IO流中的类,都是"自带缓冲区"的
// 引入缓冲区之后,进行写入操作,不会立即触发IO,而是先放到内存缓冲区中,等缓冲区里攒了一些后,再统一进行发送
// 由于此处的数据比较少,因此这样的数据就会一直停留在缓冲区中,出不去了~
// 为了让较少的数据也能发送出去,这里就可以引入flush操作,来主动"刷新缓冲区".
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 {
D server = new D(9090);
server.start();
}
}
客户端代码:
package javaEE.Internet;
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 E {
private Socket socket = null;
public E(String serverIp, int severPort) throws IOException {
socket = new Socket(serverIp, severPort);
}
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 (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
E client = new E("127.0.0.1", 9090);
client.start();
}
}
运行效果:
本文到这里就结束啦~