Socket 翻译过来中文含义有(电源)插座;(电器)插口, 插孔等含义, 也就是说在电源领域, 我们将 Socket 称之为插座, 在电器领域, 我们将 Socket 称之为插孔, 那我们是不是就可以理解为在计算机通信领域, 担任插座或者插孔这个含义的 Socket 就定义成了套接字呢?
理论上 Socket 套接字指的是由系统提供的用于网络通信的一种技术, 是基于 TCP / IP 协议的网络通信的基本单元, 也就是说基于 Socket 套接字的网络程序的开发就是网络编程; 看着这段话比较绕, 其实总结一句话: Socket 套接字就是为了实现网络编程的一组 API, 并遵循一定的约定和规则.
关于 Socket 的分类主要有三种, 这三种也是主要针对传输层协议划分的;
TCP 特性: 有连接 / 可靠传输 / 面向字节流 / 有接收缓冲区 / 有发送缓冲区 / 大小不限;
UDP 特性: 无连接 / 不可靠传输 / 面向数据报 / 有接收缓冲区 / 无发送缓冲区 / 大小受限(一次最多传输 64 k);
对于 UDP 协议来说, 具有无连接, 面向数据报的特征, 因此每次都是没有建立连接, 并且一次性发送全部数据报, 一次性接收全部的数据报; java 中使用 UDP 协议通信主要基于 DatagramSocket 类来创建数据报套接字, 并使用 DatagramPacket 作为发送或接收的 UDP 数据报, 对于一个服务端而言, 重要的是提供多个客户端的请求处理及相应, 主要流程如下:
这里我们简单写一个回显服务器当做 来解读流套接字编程用到的方法和逻辑, ServletSocket 是创建 TCP 服务端 Socket 的 API, 主要处理客户端的连接, Socket 主要用来和客户端进行具体的交互, 这里还需要注意 TCP 协议有连接, 类似于打电话; 此处我们实现的是一个长连接版本的服务器, 关于长连接和短连接的区别, 后面会解释.
服务端代码如下:
public class TcpEchoServer {
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void star() throws IOException {
System.out.println("服务器启动啦!");
while (true) {
Socket clientSocket = serverSocket.accept();
processConnection(clientSocket);
}
}
private void processConnection(Socket clientSocket) {
System.out.printf("[%s:%d] 客户端上线\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort());
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))) {
while (true) {
String request = bufferedReader.readLine();
bufferedWriter.write(reponse + "\n");
bufferedWriter.flush();
System.out.printf("[%s:%d] req: %s; resp: %s\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort(), request,reponse);
}
} catch (IOException e) {
System.out.printf("[%s:%d] 客户端下线\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort());
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.star();
}
}
代码流程解读:
客户端代码如下:
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient (String serverIp, int serverPort) throws IOException {
socket = new Socket(serverIp,serverPort);
}
public void start() {
System.out.println("客户端启动了!");
Scanner scanner = new Scanner(System.in);
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))){
while (true) {
System.out.println("--> ");
String request = scanner.nextLine();
if("exit".equals(request)) {
break;
}
bufferedWriter.write(request + "\n");
bufferedWriter.flush();
String response = bufferedReader.readLine();
System.out.println(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();
}
}
UDP 套接字编程相比于 TCP 编程最大的区别在于发送和接收 UDP 数据报用到的方法是 DatagramSocket(), 其方法和使用我们用例子来解释; 这里我们还是写一个简单回显功能的服务器当做 , 客户端发送什么, 服务端就显示什么.
服务器端代码如下:
public class UdpEchoServer {
private DatagramSocket socket = null;
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
while(true) {
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
socket.receive(requestPacket);
String request = new String(requestPacket.getData(),
0, requestPacket.getLength()).trim();
String response = process(request);
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),
response.getBytes().length,requestPacket.getSocketAddress());
socket.send(responsePacket);
System.out.printf("[%s:%d] req: %s; resp: %s\n",requestPacket.getAddress().toString(),
requestPacket.getPort(),request,response);
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIp;
private int serverPort;
public UdpEchoClient(String serverIp,int serverPort) throws SocketException {
this.serverIp = serverIp;
this.serverPort = serverPort;
socket = new DatagramSocket();
}
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
while(true) {
System.out.print("-->");
String request = scanner.nextLine();
if(request.equals("exit" +
"")) {
break;
}
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),
request.getBytes().length, InetAddress.getByName(serverIp),serverPort);
socket.send(requestPacket);
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096 );
socket.receive(responsePacket);
String response = new String(responsePacket.getData(),0,responsePacket.getLength());
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);
client.start();
}
}
其实上面 TCP 的演示就用到了自定义协议, 当客户端发送请求的时候我们加上了 “\n”, 然后又在服务器读取的时候约定了 readLine(), 这里的按行写和按行读这样的过程就是一种最简单的自定义协议, 当然我们也可以约定为其他形式. 如下所示:
设想一下, 如果一次往 IO 设备中写一个字节, 分 100 次写的效率远远低于一次往 IO 设备中写 100 个字节分一次性写完的效率, 这样算的话效率差距就近乎 100 倍; 因此如果让写操作先把数据写到内存中, 然后当内存中数据达到一定程度时, 再统一写入到 IO 设备中;
操作 IO 设备程序的效率很大程度上取决于程序真正访问 IO 设备的次数, 因此缓冲区存在的真正意义就是为了减少访问 IO 设备的次数.
(1) 如何基于 UDP 协议实现可靠传输???
虽然问的是 UDP, 但实际操作起来还是 TCP 相关知识!!!
(2) 什么样的场景适合使用 TCP, 什么样的场景适合 UDP???
(3) 像 CF, LOL, 王者荣耀这一类游戏传输的时候优先使用 TCP 还是 UDP??
很有可能既不是 TCP 也不是 UDP; 因为传输层的协议不只是只有这两种协议, 还有很多的传输层协议, 有的协议就可以尽可能的兼顾到可靠性和效率.