网络编程套接字(socket)
套接字
什么是socket:
所谓的套接字也就是socket,这个socket和我们编写网络程序又有什么关系呢:
socket是传输层给应用层提供的API的集合,socket就相当于是程序使用网络的接口,而网卡是把数据发出去的硬件,socket就相当于是网卡的一个遥控器
我们网络编程主要涉及的就是传输层以及应用层的交互,我们先决定传输层的哪一个协议,是UDP还是TCP,也就是上文所介绍的两种:
后续的各种层是操作系统根据我们代码中的任务来选择的
发送数据的过程:
Java 程序
↓
Java Socket 对象
↓
文件描述符 fd
↓
进程的文件描述符表
↓
操作系统内核中的 Socket 对象
↓
TCP / IP 协议栈
↓
网卡
↓
网络
为什么会有文件描述符表:明明我们是讲socket,为什么会有一个文件描述符表呢,原因就是在Linux中很多东西都被抽象成了“可以像文件一样读写的资源”,所以他们统一通过文件描述符表(fd)来管理,但是我们学习的并不是linux操作系统啊,为什么会有什么个说法,因为在基本上所有的操作系统中,都有这么一个概念,都会让程序拿到一个编号,然后通过这个编号来找到底层系统中的操作以及方法,只不过在linux里面叫做文件描述符表,所以我们可以统一叫做这个来理解,还有就是为什么我们不能直接通过Java代码来操作一切,因为网络、文件、进程这些资源都属于操作系统的重要资源,不能让普通程序随便直接改,所以需要通过一个像文件描述符表的东西进行调度,调度到底层的操作系统的socket
socket的组成:
Socket(网络通信接口,大概念)
│
├── TCP
│ ├── Socket
│ └── ServerSocket
│
└── UDP
├── DatagramSocket
└── DatagramPacket
UDP
DatagramSocket = 通信工具
DatagramPacket = 要传输的数据包
DatagramSocket
| 构造方法 | 作用 | 常见场景 |
|---|---|---|
DatagramSocket() | 创建一个 UDP Socket,端口由系统自动分配 | 客户端常用 |
DatagramSocket(int port) | 创建 UDP Socket,并绑定指定端口 | 服务器常用 |
DatagramSocket(int port, InetAddress laddr) | 绑定指定端口和本机 IP | 一台机器有多个网卡/IP 时使用 |
| 方法 | 作用 |
|---|---|
send(DatagramPacket p) | 发送 UDP 数据包 |
receive(DatagramPacket p) | 接收 UDP 数据,并保存到 p 中 |
close() | 关闭 Socket,释放网络资源 |
getLocalPort() | 获取当前 Socket 绑定的本地端口 |
getLocalAddress() | 获取当前 Socket 绑定的本地 IP |
isClosed() | 判断 Socket 是否已经关闭 |
DatagramPacket
1,接收数据:
new DatagramPacket(byte[] buf, int length)
| 参数 | 作用 |
|---|---|
buf | 用来存放收到的数据 |
length | 最多允许接收多少字节 |
2,发送数据:
new DatagramPacket(
byte[] buf,
int length,
InetAddress address,
int port
)
| 参数 | 作用 |
|---|---|
buf | 要发送的数据 |
length | 要发送多少字节 |
address | 对方的 IP 地址 |
port | 对方的端口号 |
回显服务器例子
注意一
为什么服务器要指定端口,而客户端不需要,因为一台服务器是用来服务很多的客户端的,指定端口是让客户端知道并且只传入到那一个端口里面去,但是客户端有很多,就得系统随机,并且有些同学很容易将一个概念混淆,就是觉得客户端的端口和服务器的端口得一致才能对接上,实际是不是的:这里也会涉及前文讲到的五原组

我们只要明确了这五个就行,不需要一致
不是说需要五元组吗,那为什么服务器里面好像少了一个自己的ip:因为系统会根据客户端的数据进行匹配
比如客户端发送:
目标 IP = 192.168.1.10
目标端口 = 9090
协议 = UDP
到我们电脑之后,系统看到的信息就是
目的 IP:192.168.1.10
目的端口:9090
协议:UDP
假设你服务器中写到:
new DatagramSocket(9090);
也就相当于是:
监听:所有本机 IP 的 UDP 9090
然后进行匹配,最终就可以得出服务器自己的ip
所以说五元组往往是客户端和服务器两边的信息共同组成的!!!!!!!!
注意二
局域网 ≈ 一个小区
IP ≈ 楼号 / 房屋地址
端口 ≈ 房间号
所以为什么会出现端口号冲突,也就是我们现在写的程序是在自己的电脑上面,所以ip都是127.0.0.1,所以如果服务器和客户端的端口号也一致的情况下,就会产生端口号冲突,也就是走到了一个小区的一个楼层中的一个房间,那我们电脑上有那么多的应用程序,为什么不会造成冲突呢,因为系统会给客户端的端口自动分配,所以我们程序里面的客户端的代码也没有指定端口号,简单介绍一下电脑里的ip
不同 IP 的来源不一样:
127.0.0.1:操作系统按照网络协议标准预留的,本机固定使用。
192.168.x.x 这种局域网 IP:一般是你家路由器通过 DHCP 自动分配给电脑。
公网 IP:通常由运营商分配给你的路由器/网络出口。
VPN 的 IP:由 VPN 软件或 VPN 服务器分配。
所以我们的应用程序的ip都是通过操作系统自己分配的,比如你点开来一个网站,系统会识别这个请求是从WiFi网卡发出去的,所以会分配192开头的那个
具体代码
package network;
//服务器
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.nio.charset.StandardCharsets;
public class UdpEchoServer {
//首先创建一个socket对象
DatagramSocket serverSocket=null;
//通过构造方法来完整
//创建一个服务器对象,需要指定端口
public UdpEchoServer(int port) throws Exception{
serverSocket=new DatagramSocket(port);
}
//开始方法
public void start() throws IOException {
System.out.println("服务器启动了");
while (true) {
//先得创建一个datagrampacket对象,这个对象是传输数据的基本单位,就相当于你赶海一人拿一个桶是一样的
DatagramPacket requestPacket = new DatagramPacket(new byte[1024], 1024);
//接收请求
serverSocket.receive(requestPacket);
//将请求转为字符串
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
//根据请求计算响应
String response = process(request);
//将响应封装到datagrampacket对象中,并且返回
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
requestPacket.getSocketAddress());
//requestPacket.getSocketAddress()不仅拿到了ip也拿到了端口号
serverSocket.send(responsePacket);
//打印日志
System.out.printf("客户端:%s 请求:%s 响应:%s\n", requestPacket.getSocketAddress(), request, response);
}
}
private String process(String request) {
//因为回显,所以就原路返回就行
return request;
}
public static void main(String[] args) throws Exception {
UdpEchoServer udpEchoServer=new UdpEchoServer(9090);
udpEchoServer.start();
}
}
package network;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Scanner;
//客户端
public class UdpEchoClient {
//也是先创建一个socket对象
DatagramSocket clientSocket=null;
//需要给定服务器的ip以及端口
private String serverIp;
private int serverPort;
public UdpEchoClient(String serverIp,int serverPort) throws SocketException {
clientSocket=new DatagramSocket();
this.serverIp=serverIp;
this.serverPort=serverPort;
}
public void start() throws Exception{
while(true){
Scanner scanner=new Scanner(System.in);
System.out.println("请输入要发送的内容:");
String request=scanner.nextLine();
if (request.equals("exit")){
break;
}
DatagramPacket requestPacket=new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(serverIp),serverPort);
clientSocket.send(requestPacket);
DatagramPacket responsePacket=new DatagramPacket(new byte[1024],1024);
clientSocket.receive(responsePacket);
//将服务器返回的数据打印出来
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
// 4. 显示响应.
System.out.println(response);
}
}
public static void main(String[] args) throws Exception {
UdpEchoClient udpEchoClient=new UdpEchoClient("127.0.0.1",9090);
udpEchoClient.start();
}
}
注意三
别人要访问我的服务器:
本机测试用 127.0.0.1,想让互联网中的别人访问,通常需要“公网 IP + 开放端口”,云服务器就是最常见的实现方式。
127.0.0.1表示当前这台电脑自己。
查字典案例
我们刚刚通过一个回显的案例来初步了解了一下UDP的构造以及相关socket的api,那我们现在再用一个查字典的案例来进一步了解一下
注意一
服务器以及客户端的主要流程
服务器大体
在while循环里面
第一步:读取请求并解析
(因为我们这里是需要返回一个字符串,所以转为字符串格式)

第二步:根据请求计算相应
(这里以回显为例子,所以只需要原路返回就行)
![]()

第三步:把相应返回给客户端

客户端大体
第一步:在控制台读取输入

第二步:构造请求发送给服务器

第三步:接收服务器发来的响应

第四步:根据返回来的响应进行显示结果

所以我们可以总结一下:我们写服务器以及客户端,只需要跟着上面的步骤,在发送和接收的时候,来书写对的DatagramPacket对象,使用正确的构造方法
注意二
为什么可以这么写:因为其实现在市面的大部分背单词软件都是这么写的,我们也可以上网复制中英文对应关系来使我们的回答更加全面
具体代码
package network;
import java.io.IOException;
import java.net.DatagramSocket;
import java.util.HashMap;
import java.util.Map;
public class UdpDictServer extends UdpEchoServer{
//因为我们是一个查字典,所以我们用到map来根据key来查找values
Map<String,String> map=new HashMap<String, String>();
//重写方法
public UdpDictServer(int port) throws Exception {
super(port);
//在这里传入对应的中英文关系
map.put("apple","苹果");
map.put("banana","香蕉");
map.put("cat","猫");
map.put("dog","狗");
}
@Override
public String process(String request) {
//request 是客户端传来的查询内容,是 key;getOrDefault() 返回的是这个 key 对应的 value。
return map.getOrDefault(request,"查无此词");
}
public static void main(String[] args) throws Exception {
UdpDictServer server = new UdpDictServer(9090);
server.start();
}
}
(因为这里的客户端和前面的回显服务器一样,都是发送,包装成String,然后发送回去,所以我们还是用的之前的客户端)
TCP
ServerSocket和Socket
客户端 服务器
Socket
│
│ 请求连接
├──────────────────────────→ ServerSocket
│ │
│ accept()
│ │
│ ↓
│ Socket
│ │
└──────────── TCP连接 ────────────┘
整体的关系差不多就是这样,serversocket负责将将请求引进家门(处理客户端的连接,所以只会在服务器中写到),而socket像是专门负责业务的专员(在客户端以及服务器里都会用到)
回显服务器例子
TCP的短长连接
短连接:就是连接一次,通信完就断,需要多次进行连接和断开,会影响效率,但是不会一直占着服务器资源
长连接:连接一次,可以通信很多次之后关闭,就像我们程序中写到的while循环,不用老是创建和关闭,所以效率会高,但是会长时间占着资源,可能一直占着一个线程
注意一
accept建立连接:
因为在我们没有写多线程的时候,就只有一个socket服务员,也就是只能一个线程在进行服务,后面的就得等着,所以我们通过多线程的形式,可以实现多客户端
注意二
flush:
在我们用printwriter(这个内置了缓冲区)进行写入的时候,会先将数据写进缓冲区里,数据满了之后才会将数据传输出去,所以我们得写flush,写入一些数据就将他们传输过去
TCP和UDP的有无连接
TCP的Socket socket = serverSocket.accept()这个方法,会进行连接,也就是会拿到这个socket的信息并且记录下来,所以后续就会根据这个信息给特定的socket进行交互,但是udp不会记录对方socket信息
注意三

具体代码
package network;
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 {
Socket socket=null;
TcpEchoClient(String serverName,int serverPort) throws IOException {
socket=new Socket(serverName,serverPort);
}
public void start() throws IOException {
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 printWriter = new PrintWriter(outputStream);
while (true) {
//先读取数据
System.out.println("client:请输入数据");
String request = scanner.next();
//再将读取的数据传给服务器
printWriter.println(request);
printWriter.flush();
//最后读取服务器的相应
if (!scannerNetwork.hasNext()) {
break;
}
String response = scannerNetwork.next();
//最后打印出来
System.out.println("server: " + response);
}
}catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
client.start();
}
}
package network;
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 {
//先创建一个ServerSocket对象
ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws Exception {
serverSocket = new ServerSocket(port);
}
//开始方法
public void start() throws IOException {
//先建立连接,然后由Socket进行处理
//注意一
ExecutorService executorService = Executors.newCachedThreadPool();
while (true) {
Socket socket = serverSocket.accept();
executorService.submit(() -> {
try {
processConnection(socket);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
}
private void processConnection(Socket socket) throws IOException {
System.out.printf("[%s:%d] 客户端上线!\n", socket.getInetAddress(), socket.getPort());
//tcp是面向字节流的
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
//因为tcp是面向字节流的,所以我们定义了InputStream和OutputStream,但是我们输入是一个字符串,所以我们会
//继续用到scanner来读和PrintWriter来写,并且PrintWriter可以将我们输入的字符串转为字节传输出去
Scanner scanner = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
while (true) {
if (!scanner.hasNext()) {
// 客户端关闭了连接
System.out.printf("[%s:%d] 客户端下线!\n", socket.getInetAddress(), socket.getPort());
// 假设这里有一系列的逻辑呢??
break;
}
//先接收数据
String request = scanner.next();
//再根据请求计算相应
String response = process(request);
//最后发送相应给客户端
printWriter.println(response);
//注意二
printWriter.flush();
System.out.printf("[%s:%d] 客户端请求: %s\n", socket.getInetAddress(), socket.getPort(), request);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
socket.close();
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws Exception {
TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);
tcpEchoServer.start();
}
}
查字典案例
package network;
import java.util.HashMap;
import java.util.Map;
public class TcpDictServer extends TcpEchoServer{
Map<String,String> map=new HashMap<String, String>();
public TcpDictServer(int port) throws Exception {
super(port);
map.put("apple","苹果");
map.put("banana","香蕉");
map.put("cat","猫");
map.put("dog","狗");
}
@Override
public String process(String request) {
//request 是客户端传来的查询内容,是 key;getOrDefault() 返回的是这个 key 对应的 value。
return map.getOrDefault(request,"查无此词");
}
public static void main(String[] args) throws Exception {
TcpDictServer server = new TcpDictServer(9090);
server.start();
}
}
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)