网络编程套接字,是操作系统给应用程序提供的一组 API (叫做 socket API)。
这里的 socket 可以认为是应用层 和 传输层之间的通信桥梁。
传输层的核心协议有两种,一种是 TCP, 另一种是 UDP,socket API也对应有两组。由于UDP 和 TCP 协议差异很大,所以这两组 API 的差异也大。
两种套接字的直接区别:
TCP —— 有链接,可靠传输,面向字节流,全双工。
UDP —— 无连接,不可靠传输,面向字节报,全双工。
有链接 和 无连接
有链接 —— 就像打电话,我们需要先接通,然后,我们才能进行通话。
无连接 —— 就像我们微信发消息,不需要先接通,可以直接发送过去。
可靠传输 和 不可靠传输
可靠传输 —— 知道自己发的信息,对方有没有收到,就像打电话,我们知道对方有没有收到信息。
不可靠传输 —— 不知道自己发的信息,对方有没有接收到,就像是发消息,虽然我们的消息发出去了,但是我们并不知道对方有没有接收到
面向字节流 和 面向字节报
面向字节流 —— 一个字节一个字节的传输数据。
面向数据报 —— 以数据报为单位传输(其中的数据报长度由我们自己定义)。
全双工 和 半双工
全双工 —— 一条链路双向通信。
半双工 —— 一条链路单向通信。
下面我们主要介绍 UDP 和 TCP 这两个协议。
UDP 有两个核心的类,一个是 DatagramSocket ,另一个是 DatagramPacket 。
DatagramSocket
这是 UDP 版本的 Socket 对象,代表着操作系统中的一个 socket 文件,是网卡硬件设备抽象体现。
其中有几个方法:receive()接收数据,send()发送数据,close()释放资源。
DatagramPacket
表示一个 UDP 数据报(用来封装数据)。
每次发送/接收数据,都是在传输一个 DatagramPacket 对象。
举例:实现一个最简单的客户端服务器程序,回显服务(请求是什么,响应就是什么)。
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
//源 IP:本机 IP
//源端口:服务器绑定的端口
//目的 IP:客户端的 IP
//目的端口:客户端的端口号
//协议类型:UDP
public class UdpEchoServer {
//网络编程的基础要有一个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){
//因为UDP接收的是一个数据报,所以我们要先定义一个空的数据报,来接收数据。
DatagramPacket requestPacket = new DatagramPacket(new byte[1024], 1024);
//其中的requestPacket是一个输出型参数,这个参数可以带出来数据,这个数据就是客户端传来的数据
socket.receive(requestPacket);
//先把数据报转换成字符串,为了方便处理
String request = new String(requestPacket.getData(), 0,requestPacket.getLength(), "UTF-8");
//使用process函数来实现服务器根据请求,来生成响应。
String response = process(request);
//因为UDP传输的是一个数据报,所以要生成一个要传输出去的数据报,其中包含了响应的IP和地端口号。
DatagramPacket responsePacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
requestPacket.getSocketAddress());
//响应客户端的请求。
socket.send(responsePacket);
System.out.printf("[%s,%d], request:%s, response:%s\n",
requestPacket.getAddress(), requestPacket.getPort(),request,response);
}
}
//这里我们实现的是一个回显效果,就是接收什么,响应什么。
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
//源 IP:本机 IP
//源端口:系统分配的端口
//目的 IP:服务器的 IP
//目的端口:服务器端口号
//协议类型:UDP
public class UdpEchoClient {
//网络编程的基础要有一个socket对象。
private DatagramSocket socket = null;
private String serverIP;
private int serverPort;
//使用构造函数来初始化socket对象,服务器的IP和端口号
public UdpEchoClient() throws SocketException {
socket = new DatagramSocket();
serverIP = "127.0.0.1";
serverPort = 9090;
}
public void start() throws IOException {
Scanner sc = new Scanner(System.in);
while(true){
System.out.print("-> ");
//输入要传给服务器的内容。
String request = sc.nextLine();
//要传给服务器的内容要封装成一个数据报(DatagramPacket),数据报中要有目标地址的IP和端口号
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
InetAddress.getByName(serverIP), serverPort);
//传送数据报
socket.send(requestPacket);
//接收要用数据报来接收,先构造出数据报
DatagramPacket responsePacket = new DatagramPacket(new byte[1024], 1024);
//接收服务器传来的数据报
socket.receive(responsePacket);
//把数据报中的内容变成字符串
String response = new String(responsePacket.getData(), 0, responsePacket.getLength(), "UTF-8");
System.out.printf("request: %s, response: %s\n", request, response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient();
client.start();
}
}
TCP API 中,也是涉及到两个核心的类 ServerSocket 类和 Socket 类
1) ServerSocket(专门给TCP服务器用的)使用 accept 方法和客户端建立连接。
2)Socket 既需要给服务器用,也需要给客户端用,TCP协议是面向字节流的,所以不需要有一个数据报类,这里直接用 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;
public class TcpEchoServer {
//创建ServerSocket对象,是为了建立连接和接收客户端所传的信息
private ServerSocket serverSocket = null;
//服务器要指定端口号,方便客户端找到服务器
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("程序启动!!");
while(true) {
//serverSocket 使用 accept 方法建立连接,这个方法会返回客户端的请求信息。
//用 Socket 这个类来接收客户端的信息
Socket socket = serverSocket.accept();
//因为要建立连接,processConnection 方法要执行完,也就是客户端断开链接,才能执行下一个客户端请求。
//这样就不能同时处理多个客户端的请求了,为了解决这个问题,使用了多线程,每一个线程负责一个客户端。
Thread thread = new Thread(()->{
processConnection(socket);
});
thread.start();
}
}
private void processConnection(Socket socket) {
System.out.printf("[%s, %d] 建立建立连接!!", socket.getInetAddress().toString(), socket.getPort());
//Socket 对象要使用文件来进行操作,每一个对象中都自带有一个文件。
try(InputStream inputStream = socket.getInputStream()) {
try(OutputStream outputStream = socket.getOutputStream()){
//使用Scanner类,为了简化代码,写起来更加的方便
Scanner scanner = new Scanner(inputStream);
while(true){
//如果客户端的请求接收完成,就断开链接,退出循环
if(!scanner.hasNext()){
System.out.printf("[%s, %d] 断开链接!!!", socket.getInetAddress().toString(),socket.getPort());
break;
}
//接收客户端的请求
String request = scanner.next();
//服务器对客户端的请求,做出响应
String response = process(request);
//把客服端的响应写入文件中,让客户端接收
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response);
//其中的 flush 是刷新缓冲区,为了让客户端及时接收到数据
printWriter.flush();
System.out.printf("[%s, %d], request:%s, response:%s\n", socket.getInetAddress().toString(),
socket.getPort(), request, response);
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
//关闭文件
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//服务器根据请求做出响应
private String process(String request) {
return request;
}
//启动服务器
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
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 {
//创建 Socket 对象
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(InputStream inputStream = socket.getInputStream()) {
try(OutputStream outputStream = socket.getOutputStream()){
while(true){
//客户端发出请求
String request = scanner.next();
//向文件中写入请求
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
printWriter.flush();
//接收文件中返回来的响应
Scanner scanner1 = new Scanner(inputStream);
String response = scanner1.next();
System.out.printf("request: %s, response: %s\n", request, response);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
//启动客户端
public static void main(String[] args) throws IOException {
TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1", 9090);
tcpEchoClient.start();
}
}