前言
为了实现客户端与服务端之间的连接,我们可以使用Socket通信。
在Socket通信中我们可以选择TCP协议或者UDP协议。
TCP协议(Transmission Control Protocol)
握手连接
此处客户端先通过connect向服务器主动发出连接申请同步SYN J(第一次握手),服务器接收到SYN J后发送了确认数据ACK J+1与申请同步SYN K(第二次握手),客户端接收到了数据后知道了服务器可以进行通讯请求,客户端发送确认数据ACK K+1(第三次握手)。成功后便可开始交换数据。
交换数据
请求建立后,客户端向服务端写出数据,服务端读到客户端的数据后,处理之后write给客户端,一次交互结束。
挥手结束
在连接关闭的时候,服务器和客户端之间将进行四次挥手,第一次由客户端发送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阻塞线程,
客户端
客户端也需要
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是直接进行数据传输,没有握手和挥手。
你好