Java中Socket套接字编程
本文最后更新于 148 天前,其中的信息可能已经有所发展或是发生改变。

前言

为了实现客户端与服务端之间的连接,我们可以使用Socket通信。

在Socket通信中我们可以选择TCP协议或者UDP协议。

TCP协议(Transmission Control Protocol)

握手连接

三次握手连接

此处客户端先通过connect向服务器主动发出连接申请同步SYN J(第一次握手),服务器接收到SYN J后发送了确认数据ACK J+1与申请同步SYN K(第二次握手),客户端接收到了数据后知道了服务器可以进行通讯请求,客户端发送确认数据ACK K+1(第三次握手)。成功后便可开始交换数据。

交换数据

TCP-Transport

请求建立后,客户端向服务端写出数据,服务端读到客户端的数据后,处理之后write给客户端,一次交互结束。

挥手结束

TCP-Close

在连接关闭的时候,服务器和客户端之间将进行四次挥手,第一次由客户端发送FIN M,进入终止等待1状态,服务器接收到客户端的结束请求后进入关闭等待状态,并向客户端发送确认ACK M+1与结束请求FIN N,客户端接收到确认请求后进入终止等待2状态,服务端的结束请求被客户端接收之后,由客户端同意服务端关闭请求,发送ACK N+1,客户端延时关闭,服务端接收到最后的确定后才关闭。

 

UDP协议(User Datagram Protocol)

特性

UDP(UserDatagramProtocol)是一个简单的面向消息的传输层协议,尽管UDP提供标头和有效负载的完整性验证(通过校验和),但它不保证向上层协议提供消息传递,并且UDP层在发送后不会保留UDP 消息的状态。因此,UDP有时被称为不可靠的数据报协议。如果需要传输可靠性,则必须在用户应用程序中实现。

UDP使用具有最小协议机制的简单无连接通信模型。UDP提供数据完整性的校验和,以及用于在数据报的源和目标寻址不同函数的端口号。它没有握手对话,因此将用户的程序暴露在底层网络的任何不可靠的方面。如果在网络接口级别需要纠错功能,应用程序可以使用为此目的设计的传输控制协议(TCP)。

综上所述:

UDP是基于IP的简单协议,不可靠的协议。

UDP的优点:简单,轻量化。

UDP的缺点:没有流控制,没有应答确认机制,不能解决丢包、重发、错序问题。

这里需要注意一点,并不是所有使用UDP协议的应用层都是不可靠的,应用程序可以自己实现可靠的数据传输,通过增加确认和重传机制,所以使用UDP 协议最大的特点就是速度快。

UDP可以直接进行连接传输,不需要像TCP一样进行握手后才能连接,但是因为丢包问题和错序问题,不能用于传输一些严格要求的数据,QQ语音便是建立在UDP连接传输协议上的,因为语音偶尔丢几个包都无伤大雅,但是传输一些精确的数据包时,还是应该使用TCP。

在Java中进行Socket编程

TCP

在Java中进行Socket编程,我们需要使用java.net中的Socket。

服务器

在服务器中,首先我们要创建一个ServerSocket的对象实例,在这里我们开放2000为端口。

ServerSocket s_socket = new ServerSocket(2000);

因为服务端开启后,不一定有客户端立马连接上来,所以我们要使用Socket对象里的accept方法堵塞线程,但是堵塞了主线程后,我们只能保证一个服务端只能连接一个客户端,但是我们想要一个服务端可以连接多个客户端,那么我们可以使用多线程来实现。

while(true){
    Socket socket = s_socket.accept();
    ServerHandler handler = new ServerHandler(socket);
    handler.start();
    System.out.println("Connecting IP:"+socket.getInetAddress().getHostAddress());
}

我使用的while(true)来轮询,第一次accept阻塞后,死循环不会向下执行,当客户端连接到服务端后,服务端accept被动开启,使得代码可以继续向下执行,这里的ServerHandler是创建的继承了Thread的类,将在下面提到,传入socket套接字后开启新线程,输出语句。

然后是ServerHandler类的实现。继承了Thread后便可以使用start()开启新线程,这里我们先传入Socket。

private Socket socket;

public ServerHandler(Socket socket){
    this.socket = socket;
}

然后覆写Thread的run方法。

@Override
public void run() {
    try {
        BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        String info = null;
        while((info = br.readLine()) != null){
            System.out.println(socket.getInetAddress().getHostAddress()+":"+info);
        }
        //socket.shutdownInput(); // 告诉客户端,输入流关闭,不必输入了,流程结束,关闭连接
        PrintWriter pw = new PrintWriter(socket.getOutputStream());
        pw.println("I'm a Server");
        pw.println("You said "+info);
        pw.flush();
        socket.shutdownOutput(); // 告诉客户端,我已经不会输出了!客户端的Reader就可以开始读取流了
        System.out.println("Ending!");
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

这里使用shutdownOutput()/shutdownInput()是防止线程卡死,因为服务端不知道客户端的话说完了没有,如果客户端没有进行shutdownOutput()操作,则需要服务端自行调用ShutdownInput()来关闭流。此处的shutdownOutput()是告诉客户端,我的输出流关闭了,你可以向下执行了。

客户端

因为客户端只需要用一个客户端对应一个服务端,不需要用多线程。

连接客户端我们需要创建Socket。

Socket socket = new Socket("localhost",2000);
public static void main(String[] main) throws IOException, InterruptedException {
    Socket socket = new Socket("localhost",2000);
    PrintWriter pw = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()));
    pw.println("Hello World!");
    pw.flush();
    socket.shutdownOutput(); // 告诉服务器,我已经不会输出了!服务器的Reader就可以开始读取流了
    BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
    String temp = null;
    while((temp = br.readLine()) != null) {
        System.out.println(socket.getInetAddress().getHostAddress() + ":" + temp);
    }
    //socket.shutdownInput(); // 告诉服务器,输入流关闭,不必输入了,流程结束,关闭连接
    System.out.println("Ending!");
    socket.close();
}

这里创建了Socket对象,连接到localhost:2000。将Socket的输出流指向PrintWriter,使用println向流中输出字符串,这里用println是因为在这里输出字符串后会自动换行,服务器中读取字符串是检测该行是否为null。输出流关闭后告诉服务端我的流关了,服务端可以向下执行了,于是服务端开始向客户端输出流写出字符串,客户端读取到字符串。

Client:

127.0.0.1:I’m a Server
127.0.0.1:You said null
Ending!

 

Server:

Start Listening:
Connecting IP:127.0.0.1
127.0.0.1:Hello World!
Ending!

UDP

Java使用UDP协议,需要用到DatagramSocket与DatagramPacket类。

服务器

在服务器中,我们也应该按照一对多来使用多线程。UDP的服务器创建Socket需要使用

DatagramSocket socket = new DatagramSocket(8080);

这里指定了端口为8080,如果想要使用bind方法,则应该使用

DatagramSocket socket = new DatagramSocket(null);
socket.bind(new InetSocketAddress("localhost",8080));

套接字创建完成后,我们需要做的就是轮询是否有客户端向服务端发送数据包了

byte[] buf = new byte[1024];
DatagramPacket receivePacket = new DatagramPacket(buf,buf.length); // 创建接收包
while(true){
    socket.receive(receivePacket); // 若没有收到包 则阻塞该进程 推荐多线程 类似上述例子
    new Thread(()->{
            String pac = new String(buf,0,receivePacket.getLength()); // 将收到的数据转成String
            System.out.println(receivePacket.getAddress().getHostAddress()+":"+pac);
            String info = Thread.currentThread().toString()+"I'm a Server!";
            DatagramPacket sendPacket = new DatagramPacket(info.getBytes(StandardCharsets.UTF_8),info.getBytes().length,receivePacket.getSocketAddress());
        try {
            socket.send(sendPacket);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }).start(); // 新线程
}

我们可以发现,UDP就不是accept来阻塞线程了,而是使用receive阻塞线程,

因为UDP可以直接进行连接传输,不需要像TCP一样进行握手后才能连接

客户端

客户端也需要

DatagramSocket socket = new DatagramSocket();

这里如果不传入参数则会自动分配端口创建UDP Socket,而TCP的Socket客户端连接服务端是直接连接至服务端地址的。这里就要提到DatagramPacket了。

DatagramPacket packet = new DatagramPacket(byte[] buf,int length);
DatagramPacket packet = new DatagramPacket(byte[] buf,int length,SocketAddress address);
DatagramPacket有几种初始化方式,这里只提这两种:
-第一种用于Socket.receive(packet)来存储接收的包的信息。
-第二种则包含了这个包的发送信息,address表示这个包将发到哪个地址。

使用socket.send(packet)即可将包含目标地址的packet发送到指定目标地址,所以我们在服务端可以这样写。

InetSocketAddress serverAddress = new InetSocketAddress("localhost",8080);
String info = "I'm Client!";
DatagramPacket sendPacket = new DatagramPacket(info.getBytes(StandardCharsets.UTF_8),info.getBytes().length,serverAddress);
DatagramSocket socket = new DatagramSocket();
socket.send(sendPacket);
byte temp[] = new byte[1024];
DatagramPacket receivePacket = new DatagramPacket(temp,temp.length);
socket.receive(receivePacket); // 若没有收到包 则阻塞该进程
System.out.println(receivePacket.getAddress().getHostAddress()+":"+new String(temp,0,receivePacket.getLength()));

Client:

127.0.0.1:Thread[Thread-0,5,main]I’m a Server!

 

Server:

127.0.0.1:I’m Client!

这就是UDP传输协议的demo,相比TCP更加简洁,两者各有所长。

总结

TCP适合用于游戏服务器与客户端的连接,群组聊天,文件传输等等。

UDP适合用于语音聊天,部分小游戏服务器和客户端的连接等等。

 

TCP有三握手和四挥手,而UDP是直接进行数据传输,没有握手和挥手。

评论

  1. 博主
    Windows Edge
    1 年前
    2023-4-21 11:48:57

    你好

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇