Hello, Hello~ 亲爱的朋友们👋👋,这里是E绵绵呀✍️✍️。 如果你喜欢这篇文章,请别吝啬你的点赞❤️❤️和收藏📖📖。如果你对我的内容感兴趣,记得关注我👀👀以便不错过每一篇精彩。 当然,如果在阅读中发现任何问题或疑问,我非常欢迎你在评论区留言指正🗨️🗨️。让我们共同努力,一起进步!
之前提过在网络编程中,应用层到运输层需要显式调用 API,而运输层到网络层以及后续层的通信通常是自动处理的,不需要开发者主动调用 API,反过来也是同理。
这里显示调用的API就是socket(套接字) ,只有调用了它,数据才能运输成功。
由于传输层提供了两个最核心的协议:UDP TCP。 因此,socket api 中也提供了两种风格来面对UDP和TCP。(传输层用哪种协议就用哪种风格去应对)
对于socket 不仅是个API,本质上是一种特殊的文件.Socket 就属于是把"网卡"这个设备,给抽象成了文件了往 socket 文件中写数据,就相当于通过网卡发送数据.从 socket 文件读数据,就相当于通过网卡接受数据
首先在了解socket用两种风格去面对UDP和TCP时,我们还要先知道udp和tcp的区别:
TCP的特点: 有连接 可靠传输 面向字节流 全双工
UDP的特点: 无连接 不可靠传输 面向数据报 全双工
连接: 此处说的"连接"不是物理意义的连接,而是抽象,虚拟的连接。 计算机中,这种 抽象 的连接是很常见的,此处的连接本质上就是建立连接的双方,各自保存对方的信息,两台计算机建立连接,就是双方彼此保存了对方的关键信息(ip和端口) 所以这也意味着如果协议是tcp的话,必须要先建立好连接才能传输信息,如果是udp,就不用建立连接就能传输消息。
可靠传输/ 不可靠传输: 网络上存在的"异常情况"是非常多的,无论使用什么样的软硬件的技术手段无法100%保证网络数据能够从A一定传输到。此处谈到的"可靠传输",尽可能的完成数据传输。虽然无法确保数据到达对方,至少可以知道,当前这个数据对方是不是收到了。 此处谈到的可靠传输,主要指的是发的数据到没到,发送方能够清楚的感知到。
面向字节流/面向数据报 面向字节流:此处谈到的字节流和文件中的字节流完全一致—— TCP(网络中传输数据的基本单位就是字节) 面向数据报:udp每次传输的基本单位是一个数据报(由一系列的字节构成的),它是特定的结构(之前讲过)。
全双工/半双工: 全双工通信允许数据同时在两个方向上传输。这意味着通信双方可以同时发送和接收数据,而不会互相干扰。 半双工通信允许数据在两个方向上传输,但不能同时进行。通信双方只能交替地发送和接收数据。
这只是简单的讲述一下,后面讲网络原理会讲到更多的本质内容。
udp时的socket相较于tcp,要相对简单,我们先讲这个
DatagramSocket 是UDP时的Socket,用于发送和接收UDP数据报。
DatagramSocket 构造方法
DatagramSocket 方法:
DatagramPacket是UDP Socket发送和接收的数据报(UDP面向数据报,每次发送接收数据的基本单位,就是一个UDP数据报) DatagramPacket 构造方法:
由于udp是没建立连接的,所以每次发送都要标明要发送的对方ip和端口。(如果建立了连接就不需要发送ip和端口)
DatagramPacket 方法:
构造UDP发送的数据报时,需要传入 SocketAddress ,该对象可以使用 InetSocketAddress 来创建。
注:这里编写的客户端服务器是一个简单 UDP 版本的服务器,称之为:回显服务器。 一个普通的服务器: 收到请求,根据请求计算响应(业务逻辑),返回响应。 回显服务器: 省略了普通服务器的 “根据请求计算响应”(这里的响应一般非常复杂),这里只是为了演示 socket api 的用法。
这里有一个重点要说下: 对于一个服务器来讲,我们需要让其绑定一个明确的端口号,因为在服务器在网络传输中处于一个被动的状态,没有一个明确的端口号,客户端就无法寻找到请求的服务器。 而对于一个客户端来说,我们可以让其随机分配一个端口号给他,不要指定,因为客户端是客户使用,不像我们程序员知道怎么分配端口,客户不清楚怎么分配端口,万一客户指定的端口被别人占用就会报错,它还不知道怎么修改,所以采用随机分配。(随机分配的都是没被占用的端口号) 以后的服务器客户端 端口号都遵循这规定
服务器端的逻辑如下:
DatagramSocket
对象,并绑定到指定的端口(10003),用于接收和发送数据报。
while (true)
循环中,创建一个 DatagramPacket
对象,用于接收客户端发送的数据报。调用 datagramSocket.receive(datagramPacket)
方法等待并接收数据报。
process
方法处理该字符串。在这个例子中,process
方法只是简单地返回传入的字符串本身。
DatagramPacket
对象,包含要发送的数据、目标地址和端口(即客户端的地址和端口)。使用 datagramSocket.send(datagramPacket1)
方法将响应数据报发送回客户端。
while (true)
循环中不断重复,服务器持续等待接收新的数据报并响应,直到程序被手动终止。
public class Serve {
public static void main(String[] args) throws IOException,InterruptedException {
System.out.println("服务器上线");
DatagramSocket datagramSocket=new DatagramSocket(10003);
while(true){
DatagramPacket datagramPacket=new DatagramPacket(new byte[4096],4096);
datagramSocket.receive(datagramPacket);
String string=new String(datagramPacket.getData(),0,datagramPacket.getLength());
byte[] bytes1=process(string).getBytes();
DatagramPacket datagramPacket1 = new DatagramPacket(bytes1,bytes1.length,new InetSocketAddress(datagramPacket.getAddress(),datagramPacket.getPort()));
datagramSocket.send(datagramPacket1);
}
}
public static String process(String string){
return string;
}
}
客户端设计逻辑:
DatagramSocket
对象,用于发送和接收数据报。这个套接字没有绑定到特定的端口,因此会使用一个临时端口。
Scanner
从控制台读取用户输入的字符串。
DatagramPacket
对象,包含要发送的数据、目标地址和端口。
DatagramSocket
发送数据报。
DatagramPacket
对象,用于接收服务器的响应。
DatagramSocket
接收数据报。
while (true)
循环中不断重复,直到程序被手动终止。
public class Client {
public static void main(String[] args) throws IOException {
System.out.println("客户端启动");
Scanner scanner=new Scanner(System.in);
DatagramSocket datagramSocket=new DatagramSocket();
while(true){
String string=scanner.next();
byte[] bytes=string.getBytes();
DatagramPacket datagramPacket1 = new DatagramPacket(bytes,bytes.length,new InetSocketAddress("192.168.50.173",10003));
datagramSocket.send(datagramPacket1);
DatagramPacket datagramPacket=new DatagramPacket(new byte[4096],4096);
datagramSocket.receive(datagramPacket);
String string1=new String(datagramPacket.getData(),0,datagramPacket.getLength());
System.out.println(string1);
}
}
}
对于这里我们要特殊说几个点:
1.对于字节数组内部存储数据报并不可能是全部都占用了,肯定有空闲空间,所以在将该字节数组变为字符串时我们肯定要把空闲空间给划掉,这时候就用datagrampacket.getlength ()得到有效长度,从而就可以忽略掉,否则会引发bug
2.仔细观察代码,发现该服务器可以同时支持多个客户端
这里有个疑问,一段代码为什么会能实现多个窗口呢?一般不是只能一段代码实现一个?这玩意我们是可以修改的。
通过这样修改,我们就能一段代码生成多个实例对象,多个窗口(客户端)。
3.对于该DatagramSocket虽然是文件,但是并不需要close,因为Datagramsocket是全程跟随该进程的,当进程结束时,我们才不会用DatagramSocket,进程只要还在运行,它依然还会用。该进程结束时文件描述表自然就销毁了,根本不用再添一个close。
ServerSocket 是创建TCP服务端Socket的API。
ServerSocket 构造方法:
ServerSocket 方法:
因为tcp是有连接的,所以在一个客户端给服务器传输信息前,我们需要先建立连接,这样才能传输信息,这时候就需要用到servesocket了。 当创建好servesocket后,如果有客户端想和服务器建立连接,这个时候服务器的应用程序是不需要做出任何操作(也没有任何感知的),系统内核直接就完成了连接建立的流程(三次握手),完成流程之后, 内核里有一个队列,可以视为是一个阻塞队列,创建好的连接对象就会在该内核的队列中(这个队列是每个 serverSocket 都有一个这样的队列) 排队. 应用程序要想和这个客户端进行通信,就需要通过一个 accept 方法把内核队列里已经建立好的连接对象,拿到应用程序中,如果没有则堵塞,有的话则会返回一个socket对象。我们可以认为上述的模型是一个生产者消费者模型。 (对于该servesocket对象可以进行多次accept,也就是一个服务器可以与多个客户端建立连接)
Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端 Socket。 不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。
这个不仅是客户端的socket的构造方法,还是客户端的连接建立请求。
以下是它的普通方法
这里的输入流和输出流就是我们发送消息的工具,它们是普通的字节流,我们之前文件io里学到的所有方法在这里也全能用,通过该输入流读取数据就能读取到另一个主机发送的数据,同理输出流就往里面写数据就能发送到另一个主机的输入流里。
这里实现的相比udp复杂一点
服务器端的逻辑如下:
ServerSocket
对象,绑定到指定的端口(10002),并监听客户端连接。
serverSocket.accept()
方法等待并接受客户端的连接请求。
Socket
对象,表示与客户端的连接。
startTask
方法处理客户端的请求。
startTask
方法中,获取客户端的输入流和输出流。
Scanner
从输入流读取客户端发送的数据。
PrintWriter
向输出流发送响应数据。
scanner.hasNext()
返回 false
(相当于客户端断开连接,之后会特殊说明这里的细节)。
process
方法进行处理(在这个例子中,process
方法只是简单地返回传入的字符串)。
finally
块中关闭客户端套接字,确保资源被正确释放。
public class TcpServe {
public static void main(String[] args) throws IOException {
System.out.println("服务器启动");
ServerSocket serverSocket = new ServerSocket(10002);
Socket socket = serverSocket.accept();
startTask(socket);
}
public static void startTask(Socket socket) throws IOException {
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner scanner = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.flush();
while (true) {
if (!scanner.hasNext())
break;
String string = scanner.next();
String string1 = process(string);
printWriter.println(string1);
printWriter.flush();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
socket.close();
}
}
public static String process(String string) {
return string;
}
}
客户端的逻辑如下:
Socket
对象,连接到指定的服务器地址(192.168.50.173)和端口(10002)。
Socket
对象的输入流和输出流,用于接收服务器的响应和发送客户端的请求。
Scanner
从控制台读取用户输入的字符串。
PrintWriter
将用户输入的字符串发送到服务器。
Scanner
从输入流读取服务器发送的响应。
while (true)
循环中不断读取用户输入,发送请求,接收响应,直到程序被手动终止或出现异常。
finally
块中关闭客户端套接字,确保资源被正确释放。
public class TcpClient {
public static void main(String[] args) throws IOException {
System.out.println("客户端上线");
Socket socket = new Socket("192.168.50.173", 10002);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner scanner = new Scanner(System.in);
Scanner scanner1 = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
while (true) {
String string = scanner.next();
printWriter.println(string);
printWriter.flush();
System.out.println(scanner1.next());
}
} catch (Exception e) {
e.printStackTrace();
} finally {
socket.close();
}
}
}
这样我们就实现了一个tcp通信:
但这里还是要说几个特殊的点:
1.这里由于我们采用的是printwrite,所以要用flush冲刷数据,否则可能有bug
2.在 TCP 网络编程中,Scamner.hasHext() 方法的行为与它在处理本地输入(如控制台输入或文件输入)时的行为有所不同。
这里由于scanner是跟另一个主机的输出流联系在一块,所以没有明确的消息边界。因此,在执行Scanner.hasnext()时如果该输入流没数据那么该方法就会阻塞,直到输入流中有数据可用则返回出true。如果客户端断开了连接,连接的输出流则结束了,断开了连接,此时就有明确的消息边界,Scanmer.hasNext()方法就因为没数据会返回 false,所以退出。(有消息边界时没数据则返回false,有数据则返回true;没消息边界时没数据则堵塞,有数据则返回true,不可能返回false)
3.对于该程序,只支持单个客户端,不支持多个客户端,如果想要达到多个,则要用多线程。
public class TcpServe {
public static void main(String[] args) throws IOException {
System.out.println("服务器启动");
ServerSocket serverSocket = new ServerSocket(10010);
while (true) {
Socket socket = serverSocket.accept();
Thread thread = new Thread(() -> {
try {
TcpServe.startTask(socket);
} catch (Exception e) {
e.printStackTrace();
}
});
thread.start();
}
}
public static void startTask(Socket socket) throws IOException {
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner scanner = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.flush();
while (true) {
if (!scanner.hasNext())
break;
String string = scanner.next();
String string1 = process(string);
printWriter.println(string1);
printWriter.flush();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
socket.close();
}
}
public static String process(String string) {
return string;
}
}
如果我们程序线程创建销毁太频繁了,还可以用线程池,这样能提高效率
public class TcpServe {
public static void main(String[] args) throws IOException {
System.out.println("服务器启动");
ServerSocket serverSocket = new ServerSocket(10017);
ExecutorService executorService = Executors.newCachedThreadPool();
while (true) {
Socket socket = serverSocket.accept();
executorService.submit(() -> {
try {
TcpServe.startTask(socket);
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
public static void startTask(Socket socket) throws IOException {
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner scanner = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.flush();
while (true) {
if (!scanner.hasNext())
break;
String string = scanner.next();
String string1 = process(string);
printWriter.println(string1);
printWriter.flush();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
socket.close();
}
}
public static String process(String string) {
return string;
}
}
4.对于socket以及它带的两个字节流文件我们都需要手动close,因为它们并不是全程跟随整个程序,有可能中途这些文件就不用了,所以就需要close对应的socket文件和字节流文件,否则一直不关而我们一直使用该程序,文件就会持续累积导致文件泄露问题。
而servesocket就不需要,因为它跟之前的Datagramsocket是全程跟随的,所以没必要多此一举。
对于这双方通信我们现在还是只限于一个主机内部,如果要两个主机进行交流就需要部署一个云服务器或者在一个局域网内部才能进行交流。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有