网络编程
网络编程套接字
网络编程套接字:是操作系统给应用程序提供的一组 API (socket API)。socket API 可以视为是 应用层 和 传输层 之间的通信桥梁。传输层的核心协议有两种,TCP UDP,socket API 也有对应的两组。由于 TCP 和 UDP 协议,差别很大,所以这两组 API 差别也挺大
- TCP:有连接,可靠传输,面向字节流,全双工。
- UDP:无连接,不可靠传输,面向数据报,全双工。
- 有链接:相当于打电话,得先接通,才能交互数据。
- 无连接:像发微信,不需要接通,直接就能发数据。
- 可靠传输:传输过程中,发送方知道接收方有没有收到数据。打电话就是可靠传输。已读也是可靠传输。
- 不可靠传输:传输过程中,发送方不知道接收方有没有收到数据。发微信就是不可靠传输。
- 面向字节流:以字节为单位进行传输(非常类似于 文件操作 中的字节流)。
- 面向数据报:以数据报为单位进行传输(一个数据报都会明确大小)。一次发生/接收必须是一个完整的数据报,不能是半个,也不能是一个半。
- 全双工:一条链路双向通信。
- 半双工:一条链路单向通信。
UDP
UDP socket 比 TCP 更简单,主要涉及到两个类:
- DatagramSocket,这个 DatagramSocket 对象对应到操作系统当中的一个 socket 文件。平时说的文件,只是指普通文件(硬盘上的数据)。实际上,操作系统中的文件还可能表示了一些硬件设备/软件资源。socket 文件,就对应着 ”网卡“ 这种硬件设备,从 socket 文件读数据,本质上就是读网卡。往 socket 文件写数据,本质上就是写网卡。可以认为 socket 文件就是一个遥控器,通过遥控器来操作网卡。核心方法:receive 接收数据 send 发送数据 close 释放资源。
- DatagramPacket,代表了一个 UDP 数据报,使用 UDP 传输数据的基本单位。每次发送/接收数据,都是在传输一个 DatagramPacket 对象。
回显服务
回想服务就是请求内容是啥,响应就是啥。我们把整个回显服务分为两部分:
- UdpEchoServer 服务器部分。
- UdpEchoClient 客户端部分。
服务器部分
- 网络编程的时候,先准备好 socket 实例。socket 实例是进行网络编程的大前提。构造 socket 对象失败的原因:
a、端口号已经被占用。
b、每个进程能够打开的文件数是有上限的,如果进程之前就已经打开了很多很多的文件,就可能导致此处的 socket 文件就不能顺利打开了。
private DatagramSocket socket = null;
- 绑定端口号,可以在运行程序的时候来指定。
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
- 多个进程不能绑定同一个端口。一个进程可以绑定多个端口,就是创建多个 socket 对象,然后绑定多个端口。
启动服务器:
- 因为 UDP 不需要建立连接,所以直接接收从客户端发来的数据即可。通过
DatagramPacket
来构造一个对象,然后通过socket.receive()
来填充数据就好。receive 可能会引起阻塞。 - 根据请求计算相应。因为这里是回显服务,所以直接返回数据就可以了。
- 把响应写回到客户端。通过 sent 方法就可以写回。返回的时候再构造一个
DatagramPacket
对象返回就好。 - 在发送数据的时候,必须指定这个数据要发给谁,就是指定 IP 和 端口。
- 一个服务器 可以有很多个客户端。产生多少客户端,要通过性能测试来看。
服务器代码如下:
public class UdpEchoServer {
//准备好 socket 实例。
private DatagramSocket socket = null;
//绑定端口号。
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
//启动服务器
public void start() throws IOException {
System.out.println("启动服务器");
while (true) {
//1、读取客户发来的请求
//为了接收数据,需要准备好一个空的 DatagramPacket 对象。
DatagramPacket requestPacket = new DatagramPacket(new byte[1024],1024);
socket.receive(requestPacket);
String request = new String(requestPacket.getData(),0,requestPacket.getLength(),"UTF-8");
//2、根据请求计算响应(由于这是一个回显服务,所以这一步就可以省略)
String response = process(request);
//3、把响应写回到客户端。
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();
}
}
客户端部分
在客户端的端口号,就不用自己手动指定了。让操作系统自己分配一个空闲的端口号就可以了。
- 客户端也要构造 UDP 请求。
- 客户端要有 IP。
- 客户端要有端口号。
客户端的主要作用:
-
从控制台读取用户输入的字符串。
-
把用户输入的内容,构造成一个 UDP 请求,并发送。构造的信息包含两部分信息:
a、数据的内容 request 字符串。
b、数据要发给谁 服务器的 IP + 端口。 -
从服务器响应数据,并解析。
-
把响应结果显示到控制台上。
客户端代码:
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIP;
private int serverPort;
public UdpEchoClient(String serverIP, int serverPort) throws SocketException {
//此处的 port 是服务器的端口
//客户端启动的的时候,不需要给 socket 指定端口,客户端自己的端口是系统自己分配的
socket = new DatagramSocket();
this.serverIP = serverIP;
this.serverPort = serverPort;
}
public UdpEchoClient() throws SocketException {
socket = new DatagramSocket();
}
public void start() throws IOException {
System.out.println("启动客户端");
Scanner scanner = new Scanner(System.in);
while (true) {
//1、从控制台读取用户输入的字符串
String request = scanner.next();
//2、把用户输入的内容,构造成一个 UDP 请求并发送
//构造的信息包含两部分信息:1、数据的内容 request 字符串 2、数据要发给谁 服务器的 IP + 端口
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(serverIP),serverPort);
socket.send(requestPacket);
//3、从服务器响应数据,并解析
DatagramPacket responsePacket = new DatagramPacket(new byte[1024],1024);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(),0,responsePacket.getLength(),"UTF-8");
//4、把响应结果显示到控制台上
System.out.printf("req: %s, resp: %s\n", request,response);
}
}
public static void main(String[] args) throws IOException {
//因为客户端和服务器在同一个端口上,使用的 Ip 仍然是 127.0.0.1 如果在不同的机器上面,就要修改 IP 了
UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);
client.start();
}
}
效果展示
先启动服务器:
再启动客户端:
测试效果:
加一些业务逻辑
通过在回显服务的基础上,加一些业务逻辑,就可以模拟实际开发当中的使用。 这里用一个翻译字典来展示:
- 客户端代码不变。
- 主要调整 process 方法。
- 读取请求并解析,把响应写回给客户端,这俩步骤都一样。
关键的就是:根据请求处理响应。只需要重写 process 方法就可以了,所以通过继承来写。
通过哈希表来构造一个词典。
代码如下:
public class TranslateServer extends UdpEchoServer{
private HashMap<String, String> translate = new HashMap<>();
public TranslateServer(int port) throws SocketException {
super(port);
translate.put("cat","小猫");
translate.put("dog","小狗");
translate.put("pig","小猪");
}
@Override
public String process(String request) {
return translate.getOrDefault(request,"没有该词的翻译");
}
public static void main(String[] args) throws IOException {
TranslateServer server = new TranslateServer(9090);
server.start();
}
}
运行效果:
TCP
TCP 是以字节流为单位的。TCP 的 API 当中主要涉及到两个类:
- ServerSocket API(专门给 TCP 服务器使用)
- Socket API(既要给服务器用,又要给客户端用)
回显服务
服务器部分
建立连接
因为 TCP 是有链接的,不能一上来就读数据,而要先建立连接(接电话)。通过 Socket 里面的 accept 来建立链接:
public void start() throws IOException {
System.out.println("服务器启动");
while (true) {
//“接电话” 建立连接。
Socket clientSocket = serverSocket.accept();
processConnection(clientSocket);
}
}
处理请求
处理请求的时候,先获得 IP 和端口号。 针对 TCP socket 的读写就和文件读写是一模一样的。
处理请求代码部分:
private void processConnection(Socket clientSocket) {
System.out.printf("[%s:%d] 客户端建立连接\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
//处理请求和响应
try (InputStream inputStream = clientSocket.getInputStream()) {
try (OutputStream outputStream = clientSocket.getOutputStream()) {
//循环的处理每个请求,然后分别返回响应
Scanner scanner = new Scanner(inputStream);
while (true) {
//1、读取请求
if (!scanner.hasNext()) {
System.out.printf("[%s:%d] 客户端断开连接",clientSocket.getInetAddress().toString(),clientSocket.getPort());
break;
}
String request = scanner.next();
//2、根据请求,计算响应
String response = process(request);
//3、把响应返回客户端,为了方便,可以使用 PrintWriter 把 OutputStream 包裹一下
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response);
//刷新缓冲区,如果不刷新的话,可能客户端就不能第一时间看到响应结果
printWriter.flush();
System.out.printf("[%s:%d] req: %s, resp: %s\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort(),request,response);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
//关闭资源
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
服务器端代码
public class TcpEchoServer {
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() 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 (InputStream inputStream = clientSocket.getInputStream()) {
try (OutputStream outputStream = clientSocket.getOutputStream()) {
//循环的处理每个请求,然后分别返回响应
Scanner scanner = new Scanner(inputStream);
while (true) {
//1、读取请求
if (!scanner.hasNext()) {
System.out.printf("[%s:%d] 客户端断开连接",clientSocket.getInetAddress().toString(),clientSocket.getPort());
break;
}
String request = scanner.next();
//2、根据请求,计算响应
String response = process(request);
//3、把响应返回客户端,为了方便,可以使用 PrintWriter 把 OutputStream 包裹一下
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response);
//刷新缓冲区,如果不刷新,可能客户端就不能第一时间看到响应结果
printWriter.flush();
System.out.printf("[%s:%d] req: %s, resp: %s\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort(),request,response);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
//关闭资源
clientSocket.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();
}
}
客户端部分
也用普通的 socket 即可。不用手动给客户端绑定端口,系统会自动分配。
- 对于 TCP 的 ServerSocket 来说,构造方法指定的端口,也表示自己绑定哪个端口。
- 对于 TCP 的 Socket 来说,构造方法指定的端口。表示要链接的服务器的端口。
流程和 UDP 一样,不过因为是字节流,所以是用文件来操作的。代码如下:
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String serverIP, int serverPort) throws IOException {
//获得 IP 和端口
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) {
// 1、从控制台读取字符串
String request = scanner.next();
// 2、根据读取的字符串,构造请求,把请求发给服务器
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
printWriter.flush();
// 3、从服务器读取响应,并解析
Scanner respScanner = new Scanner(inputStream);
String response = respScanner.next();
// 4、把结果显示到控制台上
System.out.printf("res: %s, resp: %s\n",request, 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();
}
}
效果展示
客户端:
服务器端:
多客户端回显服务多线程版本
因为上面的版本,一次只能接通一个会话,也就是一次只能连接一个客户端,所以实现一个多客户端的。通过多线程就可以实现多客户端了。其他地方都不要修改。只需要修改接通链接的地方就好了。修改后的代码如下:
public class TcpThreadEchoServer {
private ServerSocket serverSocket = null;
public TcpThreadEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
//建立连接
while (true) {
Socket clientSocket = serverSocket.accept();
//每次 accept 成功,都创建一个新的线程,由新线程去执行这个 processConnection
Thread t = new Thread(() -> {
processConnection(clientSocket);
});
t.start();
}
}
private void processConnection(Socket clientSocket) {
System.out.printf("[%s:%d] 客户端建立连接\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
//处理请求和响应
try (InputStream inputStream = clientSocket.getInputStream()) {
try (OutputStream outputStream = clientSocket.getOutputStream()) {
//循环的处理每个请求,然后分别返回响应
Scanner scanner = new Scanner(inputStream);
while (true) {
//1、读取请求
if (!scanner.hasNext()) {
System.out.printf("[%s:%d] 客户端断开连接",clientSocket.getInetAddress().toString(),clientSocket.getPort());
break;
}
String request = scanner.next();
//2、根据请求,计算响应
String response = process(request);
//3、把响应返回客户端,为了方便,可以使用 PrintWriter 把 OutputStream 包裹一下
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response);
//刷新缓冲区,如果不刷新,可能客户端就不能第一时间看到响应结果
printWriter.flush();
System.out.printf("[%s:%d] req: %s, resp: %s\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort(),request,response);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
//关闭资源
clientSocket.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();
}
}
运行结果:
我们创建三个客户端来测试:
客户端1:
客户端2:
客户端3:
服务器端:
通过多线程就可以完成多客户端访问了。在客户端 new Socket 成功的时候,其实操作系统层面已经建立好了连接(TCP 三次握手),但是应用程序并没有接通这个连接。
多客户端回显服务线程池版本
既然可以通过多线程来实现多客户端,那么也可以实现线程池版本。只要把 new 线程的对方,改成线程池就好了。
public class TcpThreadPoolEchoServer {
private ServerSocket serverSocket = null;
public TcpThreadPoolEchoServer (int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
ExecutorService pool = Executors.newCachedThreadPool();
while (true) {
Socket clientSocket = serverSocket.accept();
//通过线程池来实现
pool.submit(new Runnable() {
@Override
public void run() {
processConnection(clientSocket);
}
});
}
}
private void processConnection(Socket clientSocket) {
System.out.printf("[%s:%d] 客户端建立连接\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
//处理请求和响应
try (InputStream inputStream = clientSocket.getInputStream()) {
try (OutputStream outputStream = clientSocket.getOutputStream()) {
//循环的处理每个请求,然后分别返回响应
Scanner scanner = new Scanner(inputStream);
while (true) {
//1、读取请求
if (!scanner.hasNext()) {
System.out.printf("[%s:%d] 客户端断开连接",clientSocket.getInetAddress().toString(),clientSocket.getPort());
break;
}
String request = scanner.next();
//2、根据请求,计算响应
String response = process(request);
//3、把响应返回客户端,为了方便,可以使用 PrintWriter 把 OutputStream 包裹一下
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response);
//刷新缓冲区,如果不刷新,可能客户端就不能第一时间看到响应结果
printWriter.flush();
System.out.printf("[%s:%d] req: %s, resp: %s\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort(),request,response);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
//关闭资源
clientSocket.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();
}
}
运行结果如下:
客户端1:
客户端2:
客户端3:
服务器端:
实现翻译服务
和 UDP 版本的实现方法一样,不过是变成了 TCP 的模式。还是继承自线程池,代码如下:
public class TcpDictServer extends TcpThreadPoolEchoServer {
private HashMap<String, String> dict = new HashMap<>();
public TcpDictServer(int port) throws IOException {
super(port);
dict.put("cat", "小猫");
dict.put("dog", "小狗");
dict.put("pig", "小猪");
}
@Override
public String process(String request) {
return dict.getOrDefault(request,"没有此翻译");
}
public static void main(String[] args) throws IOException {
TcpDictServer server = new TcpDictServer(9090);
server.start();
}
}
运行结果如下: