前言:
大家好,我是良辰丫,今天我们一起来学习网络编程,网络编程的基本概念,认识套接字,UDP与TCP编程.💞💞💞
🧑个人主页:良辰针不戳
📖所属专栏:javaEE初阶
🍎励志语句:生活也许会让我们遍体鳞伤,但最终这些伤口会成为我们一辈子的财富。
💦期待大家三连,关注,点赞,收藏。
💌作者能力有限,可能也会出错,欢迎大家指正。
💞愿与君为伴,共探Java汪洋大海。
1. 简述网络编程
1.1 关于网络编程
网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络通信()或称为网络数据传输)
- 进程A:编程来获取网络资源
- 进程B:编程来提供网络资源
1.2 网络编程的相关概念
1.2.1 发送端与接收端
- 发送端:数据的发送方的进程,称为发送端。发送端主机即网络通信中的源主机。
- 接收端:数据的接收方进程,称为接收端。接收端主机即网络通信中的目的主机。
- 收发端:发送端和接收端两端,也简称为收发端。
1.2.2 客户端与服务器
接收服务的一方称为
客户端
.
提供服务的一方称为服务器
.
服务器为客户端提供服务,客户端接收服务器的服务.
1.2.3 请求与响应
请求与响应是客户端与服务器交互的专业术语.
- 客户端向服务器发起请求.
- 服务器对客户端的请求做出响应.
2. socket套接字
Socket套接字
,是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。基于Socket套接字的网络程序开发就是网络编程.- 程序员写程序主要编写的是应用程序,真正要发这个数据需要上层协议调用下层协议,应用层要调用传输层,传输层给应用层提供的一组api称为socket api.
- 换句话说网络socket套接字相当于一组网络接口,在网络编程中,许多功能可以直接调用socket api来实现,非常的方便.
2.1 数据报套接字(UDP)
- 数据报套接字是基于UDP协议的.
- UDP协议全称为用户数据报协议.
UDP协议的特点
- 无连接.使用UDP通信的双方,不需要刻意保存对端的信息.可以比作发短信,不用建立连接就能发送.
- 不可靠传输.消息发了不一定收到,发短信,显示发了,但是对方不一定收到.
- 面向数据报.以一个UDP数据报为基本单位.
- 全双工.一条路径,双向通信.
2.2 流套接字(TCP)
- 流套接字是基于TCP协议的.
- TCP协议全称为传输控制协议.
TCP协议的特点
- 有连接.使用TCP通信的双方,需要可以保存对方的相关信息.比如打电话需要互相建立连接,才能正常通信.
- 可靠传输.建立连接之后才能通信,很大可能性确定对方收到自己的消息,比如打电话,一人一句,这就确保了可靠性.
- 面向字节流.以字节为传输的基本单位,读写方式非常灵活.
- 全双工.一条路径,双向通信.
3. 基于UDP的网络编程
3.1 网络编程的前提知识
- Datagram就是"数据报",Socket表明自己是一个socket对象,这相当于对应到系统中一个特殊的文件(socket文件).
- socket并非对应硬盘上某个数据存储空间,而是对应网卡这个硬件设备.
关于文件
(操作系统的设计理念,一切皆文件) :
- 广义上代指许多计算机中的软件/硬件资源.
- 狭义上代指硬盘上的一块存储区域.
要想进行网络通信,就要有socket文件这样的对象,借助这个对象可以间接的操作网卡.
- 往这个socket对象里面写数据,相当于通过网卡发送消息.
- 在这个socket对象里面读数据,相当于通过网卡接收数据.
3.2 基于UDP的套接字
- DatagramSocket 是UDP Socket,用于发送和接收UDP数据报。
DatagramSocket 构造方法:
方法 | 作用 |
---|---|
DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端) |
DatagramSocket(intport) | 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务端) |
DatagramSocket 方法:
方法 | 作用 |
---|---|
void receive(DatagramPacket p) | 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) |
void send(DatagramPacketp) | 从此套接字发送数据报包(不会阻塞等待,直接发送) |
void close() | 关闭此数据报套接字 |
- DatagramPacket是UDP Socket发送和接收的数据报.
- 构造UDP发送的数据报时,需要传入 SocketAddress ,该对象可以使用 InetSocketAddress 来创建。
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 方法:
方法 | 作用 |
---|---|
InetAddressgetAddress() | 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址 |
int getPort() | 从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号 |
byte[] getData() | 获取数据报中的数据 |
InetSocketAddress ( SocketAddress 的子类 )构造方法:
方法 | 作用 |
---|---|
InetSocketAddress(InetAddress addr, int port) | 创建一个Socket地址,包含IP地址和端口号 |
- socket对象可以被客户端/服务器使用.
- 服务器这边的socket需要关联一个端口号(不能变,因为要与客户端进行匹配)
- 客户端这边不需要手动指定,系统自己分配就可以了.
- socket也是文件,文件用完了需要关闭,要不然会资源泄露.
3.3 基于UDP的编程
一个最简单的
回显服务器
,客户端向服务器发了一个请求,服务器返回一个一毛一样的响应.
3.3.1 UDP服务器
- 读取请求并解析
- 根据请求计算响应(这块稍微比较复杂,实际工作中经常用到,但是咱们回显服务器非常简单)
- 把响应返回到客户端.
package socket;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
//import java.nio.charset.StandardCharsets;
public class UdpEchoServer {
// 需要先定义一个 socket 对象.
// 通过网络通信, 必须要使用 socket 对象.
private DatagramSocket socket = null;
// 绑定一个端口, 不一定能成功!!
// 如果某个端口已经被别的进程占用了, 此时这里的绑定操作就会出错.
// 同一个主机上, 一个端口, 同一时刻, 只能被一个进程绑定.
public UdpEchoServer(int port) throws SocketException {
// 构造 socket 的同时, 指定要关联/绑定的端口.
socket = new DatagramSocket(port);
}
// 启动服务器的主逻辑.
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
// 每次循环, 要做三件事情:
// 1. 读取请求并解析
// 构造空饭盒
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
// 食堂大妈给饭盒里盛饭. (饭从网卡上来的)
socket.receive(requestPacket);
// 为了方便处理这个请求, 把数据包转成 String
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
// 2. 根据请求计算响应(此处省略这个步骤)
String response = process(request);
// 3. 把响应结果写回到客户端
// 根据 response 字符串, 构造一个 DatagramPacket .
// 和请求 packet 不同, 此处构造响应的时候, 需要指定这个包要发给谁.
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
// requestPacket 是从客户端这里收来的. getSocketAddress 就会得到客户端的 ip 和 端口
requestPacket.getSocketAddress());
socket.send(responsePacket);
System.out.printf("[%s:%d] req: %s, resp: %s\n", requestPacket.getAddress().toString(),
requestPacket.getPort(), request, response);
}
}
// 这个方法希望是根据请求计算响应.
// 由于咱们写的是个 回显 程序. 请求是啥, 响应就是啥!!
// 如果后续写个别的服务器, 不再回显了, 而是有具体的业务了, 就可以修改 process 方法,
// 根据需要来重新构造响应.
// 之所以单独列成一个方法, 就是想让同学们知道, 这是一个服务器中的关键环节!!!
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer udpEchoServer = new UdpEchoServer(9090);
udpEchoServer.start();
}
}
- receive方法中,一般使用参数作为方法的"输入",用返回值作为方法的"输出",但有时候,使用参数也能作为返回值.
- receive传一个空的packet对象,然后由方法内部把参数这个packet进行填充,这个时候packet就会有真的数据了,packet相当于一个购物车,填充东西后,购物车就不再空了.
- 服务器启动后,调用start方法,立即执行到receive方法,如果这个时候,客户端没有发来数据,receive就会进入阻塞状态.
- receive内部针对参数进行修改内容,外部也会生效.
3.3.2 UDP客户端
package socket;
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 {
// 通过这个客户端可以多次和服务器进行交互.
Scanner scanner = new Scanner(System.in);
while (true) {
// 1. 先从控制台, 读取一个字符串过来
// 先打印一个提示符, 提示用户要输入内容
System.out.print("-> ");
String request = scanner.next();
// 2. 把字符串构造成 UDP packet, 并进行发送.
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
InetAddress.getByName(serverIP), serverPort);
socket.send(requestPacket);
// 3. 客户端尝试读取服务器返回的响应
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
// 4. 把响应数据转换成 String 显示出来.
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
System.out.printf("req: %s, resp: %s\n", request, response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1", 9090);
// UdpEchoClient udpEchoClient = new UdpEchoClient("42.192.83.143", 9090);
udpEchoClient.start();
}
}
服务器与客户端交互过程:
- 服务器先启动,执行到receive进入阻塞状态.
- 客户端执行后,从控制台读取数据,进行send.
- 客户端在send之后,继续往下走,走到receive读取响应,会阻塞等待;服务器此时就从receive返回,读取请求数据(客户端发来的请求),走到process生成响应,再往下走到send,然后打印日志.
- 客户端真正收到服务器send回来的数据后,就会解除阻塞,执行打印操作;服务器进入下一轮,再次阻塞在receive,等待客户端下一个请求.
- 客户端继续进行下一轮循环,阻塞在控制台输入这里,等待用户输入新的数据.
看了上面过程,或许有点不清晰,那么咱们简化一下过程.
- 客户端读取用户输入,然后构造请求并发送.
- 服务器读取用户的请求,根据请求计算响应,把响应写回给客户端.
- 客户端读取服务器的响应,把响应转化为字符串,然后打印出来.
注意:
系统自动给客户端分配空闲端口
3.3.3 基于UDP的字典服务器
package socket;
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
// 使用继承, 是为了复用之前的代码.
public class UdpDictServer extends UdpEchoServer {
private Map<String, String> dict = new HashMap<>();
public UdpDictServer(int port) throws SocketException {
super(port);
dict.put("dog", "小狗");
dict.put("cat", "小猫");
dict.put("pig", "小猪");
}
@Override
public String process(String request) {
return dict.getOrDefault(request, "该单词没有查到!");
}
public static void main(String[] args) throws IOException {
UdpDictServer udpDictServer = new UdpDictServer(9090);
udpDictServer.start();
}
}
4. 基于TCP的编程
4.1 前提知识
关于close.
- close是一个释放资源的方法,当socket文件不再使用时,才会调用close释放资源.
- 进程结束的时候就是资源释放的时候.
TCP两个核心的类
- ServerSocket:给服务器使用
- Socket:既会给服务器使用,也会给客户端使用.
4.1.1 ServerSocket API
ServerSocket 是创建TCP服务器的套接字的API.
ServerSocket 构造方法
方法 | 作用 |
---|---|
ServerSocket(int port) | 创建一个服务端流套接字Socket,并绑定到指定端口 |
ServerSocket 方法:
方法 | 作用 |
---|---|
Socketaccept() | 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待 |
void close() | 关闭套接字 |
4.1.2 Socket API
- Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。
- 不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。
Socket 构造方法:
方法 | 作用 |
---|---|
Socket(String host, int port) | 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接 |
Socket 方法:
方法 | 作用 |
---|---|
InetAddress getInetAddress() | 返回套接字所连接的地址 |
InputStream getInputStream() | 返回套接字的输入流 |
OutputStream getOutputStream() | 返回套接字的输出流 |
4.1.3 TCP的长短连接
TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接:
短连接
:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能一次收发数据。长连接
:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可以多次收发数据。
长短连接的区别:
- 建立连接、关闭连接的耗时:短连接每次请求、响应都需要建立连接,关闭连接;而长连接只需要第一次建立连接,之后的请求、响应都可以直接传输。相对来说建立连接,关闭连接也是要耗时的,长连接效率更高。
- 主动发送请求不同:短连接一般是客户端主动向服务端发送请求;而长连接可以是客户端主动发送请求,也可以是服务端主动发。
- 两者的使用场景有不同:短连接适用于客户端请求频率不高的场景,如浏览网页等。长连接适用于客户端与服务端通信频繁的场景,如聊天室,实时游戏等
4.2 TCP服务器
为了处理多个客户端,这里的accept方法采取多线程的方式.
- 多线程.
- 线程池.
package socket;
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 就是外场拉客的小哥
// clientSocket 就是内场服务的小姐姐.
// serverSocket 只有一个. clientSocket 会给每个客户端都分配一个~
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
ExecutorService executorService = Executors.newCachedThreadPool();
System.out.println("服务器启动!");
while (true) {
Socket clientSocket = serverSocket.accept();
// 如果直接调用, 该方法会影响这个循环的二次执行, 导致 accept 不及时了.
// 创建新的线程, 用新线程来调用 processConnection
// 每次来一个新的客户端都搞一个新的线程即可!!
// Thread t = new Thread(() -> {
// try {
// processConnection(clientSocket);
// } catch (IOException e) {
// e.printStackTrace();
// }
// });
// t.start();
executorService.submit(new Runnable() {
@Override
public void run() {
try {
processConnection(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
// 通过这个方法来处理一个连接.
// 读取请求
// 根据请求计算响应
// 把响应返回给客户端
private void processConnection(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress().toString(),
clientSocket.getPort());
// try () 这种写法, ( ) 中允许写多个流对象. 使用 ; 来分割
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
// 没有这个 scanner 和 printWriter, 完全可以!! 但是代价就是得一个字节一个字节扣, 找到哪个是请求的结束标记 \n
// 不是不能做, 而是代码比较麻烦.
// 为了简单, 把字节流包装秤了更方便的字符流~~
Scanner scanner = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
while (true) {
// 1. 读取请求
if (!scanner.hasNext()) {
// 读取的流到了结尾了 (对端关闭了)
System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress().toString(),
clientSocket.getPort());
break;
}
// 直接使用 scanner 读取一段字符串.
String request = scanner.next();
// 2. 根据请求计算响应
String response = process(request);
// 3. 把响应写回给客户端. 不要忘了, 响应里也是要带上换行的.
printWriter.println(response);
printWriter.flush();
System.out.printf("[%s:%d] req: %s; resp: %s\n", clientSocket.getInetAddress().toString(),
clientSocket.getPort(), request, response);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
clientSocket.close();
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);
tcpEchoServer.start();
}
}
4.3 TCP客户端
package socket;
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 port) throws IOException {
// 这个操作相当于让客户端和服务器建立 tcp 连接.
// 这里的连接连上了, 服务器的 accept 就会返回.
socket = new Socket(serverIp, port);
}
public void start() {
Scanner scanner = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
PrintWriter printWriter = new PrintWriter(outputStream);
Scanner scannerFromSocket = new Scanner(inputStream);
while (true) {
// 1. 从键盘上读取用户输入的内容.
System.out.print("-> ");
String request = scanner.next();
// 2. 把读取的内容构造成请求, 发送给服务器.
// 注意, 这里的发送, 是带有换行的!!
//下面一行只是把数据写入内存的缓冲区中,等到缓冲区满了,才会
//真正写入网卡
//因此在这里我们需要手动写入到网卡中
printWriter.println(request);
//手动刷新缓冲区
printWriter.flush();
// 3. 从服务器读取响应内容
String response = scannerFromSocket.next();
// 4. 把响应结果显示到控制台上.
System.out.printf("req: %s; resp: %s\n", request, response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
client.start();
}
}
简单总结一下TCP过程:文章来源:https://www.toymoban.com/news/detail-418980.html
- 服务器启动,进入循环,执行到accept方法阻塞等待.
- 客户端与服务器建立TCP连接,绑定ip和端口.
- 客户端从控制端输入内容.(在此时,服务器也在监控者客户端,如果客户端下线就断开连接)
- 客户端把读取的内容打包成请求,发给服务器.
- 服务器读取请求,根据请求计算响应,把响应内容写回给客户端.
- 客户端从服务器读取响应内容,然后打印在控制台.
小结TCP:文章来源地址https://www.toymoban.com/news/detail-418980.html
- 服务器先运行start,执行到accept进行阻塞等待.
- 客户端启动,会调用socket的构造方法,和服务器建立连接,连接成功后,服务器的accept就会返回.
- 客户端往下执行控制台读取用户输入数据的时候也会阻塞等待.服务器就进入processConnection了.如果客户端没有发送请求的话,读取操作也会阻塞.
- 当客户端输入内容的时候,客户端正常把请求发出去,这个时候,执行到读取服务器响应的时候再次进入阻塞状态.
- 服务器收到客户端的请求之后,从next这里返回,执行到process与println,把响应写回给客户端.
- 客户端收到服务器的响应,就可以把结果打印到控制台,然后再次进入循环,等待用户再次输入数据.
- 服务器也会重新执行到循环位置,继续尝试读取请求,并进入阻塞状态.
到了这里,关于【网络编程】网络编程概念,socket套接字,基于UDP和TCP的网络编程的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!