网络编程套接字 -- TCP/UDP
- 一、网络编程
- 1.1 什么是网络编程
- 1.2 网络编程中的基本概念
- 1.3 TCP和UDP
- 二、UDP数据报套接字编程
- 2.1 DatagramSocket API
- 2.2 DatagramPacket API
- 2.3 InetSocketAddress API
- 2.4 回显程序 (UDP)
- 2.5 翻译程序 (UDP)
- 三、TCP流套接字编程
- 3.1 ServerSocket API
- 3.2 Socket API
- 3.3 回显程序 (TCP)
- 3.4 翻译程序 (TCP)
一、网络编程
1.1 什么是网络编程
网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络通信 (或称为网络数据传输)。
当然,我们只要满足进程不同就行;所以即便是同一个主机,只要是不同进程,基于网络来传输数据,
也属于网络编程。
1.2 网络编程中的基本概念
发送端和接收端
在一次网络数据传输时:
发送端:数据的发送方进程,称为发送端。发送端主机即网络通信中的源主机。
接收端:数据的接收方进程,称为接收端。接收端主机即网络通信中的目的主机。
收发端:发送端和接收端两端,也简称为收发端。
注意:发送端和接收端只是相对的,只是一次网络数据传输产生数据流向后的概念。
请求和响应
一般来说,获取一个网络资源,涉及到两次网络数据传输:
第一次:请求数据的发送;第二次:响应数据的发送。
好比在快餐店点一份炒饭,先要发起请求点一份炒饭;再有快餐店提供的对应响应提供一份炒饭。
客户端和服务端
服务端:在常见的网络数据传输场景下,把提供服务的一方进程,称为服务端,可以提供对外服务。
客户端:获取服务的一方进程,称为客户端。
常见的客户端服务端模型:
- 客户端先发送请求到服务端
- 服务端根据请求数据,执行相应的业务处理
- 服务端返回响应:发送业务处理结果
- 客户端根据响应数据,展示处理结果(展示获取的资源,或提示保存资源的处理结果)
1.3 TCP和UDP
网络编程套接字就是通过写代码来完成网络编程。
需要用到socket套接字,是操作系统给应用程序提供的API,也就是传输层给应用层提供的!!!
网络传输层中有很多种协议,其中最知名的就是TCP、UDP协议。
而两者工作特性差别较大,因此操作系统就提供了两个版本、风格迥异的API~~
简单说一下TCP和UDP之间的区别:
有无连接:打电话是有连接,得是通信双方建立好连接才能通信 (交互数据),即A给B打电话,B得接了才能说话;发短信/微信是无连接,直接一发就过去了~~
是否为可靠传输:可靠传输,不是说A给B发的数据100%能够让B收到 (网络环境非常复杂,无法给出100%的承诺),而是A能够知道B是不是收到了~~
字节流和数据报:字节流在文件IO的时候介绍过:博客链接,TCP和文件操作一样,也是基于流的;数据报则是将数据整合起来统一发送/接收。
全双工和半双工:全双工是双向通信;半双工是单向通信。网络通信一般都是全双工的~~
socket对应到网卡这个硬件设备,操作系统也是把网卡当作文件来管理!通过网卡发送数据就是"写文件";通过网卡接收数据就是"读文件"~~
二、UDP数据报套接字编程
2.1 DatagramSocket API
DatagramSocket 是UDP Socket,用于发送和接收UDP数据报。
DatagramSocket 构造方法:
方法签名 | 方法说明 |
---|---|
DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口 (一般用于客户端) |
DatagramSocket(int port) | 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务端) |
DatagramSocket 方法:
方法签名 | 方法说明 |
---|---|
void receive(DatagramPacket p) | 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) |
void send(DatagramPacket p) | 从此套接字发送数据报包(不会阻塞等待,直接发送) |
void close() | 关闭此数据报套接字 |
2.2 DatagramPacket API
DatagramPacket是UDP Socket发送和接收的数据报。
DatagramPacket 构造方法:
方法签名 | 方法说明 |
---|---|
DatagramPacket(byte[] buf, int length) | 构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数length) |
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) | 构造一个DatagramPacket以用来发送数据报,发送的数据为字节数组(第一个参数buf)中,从0到指定长度(第二个参数length)。address指定目的主机的IP和端口号 |
DatagramPacket 方法:
方法签名 | 方法说明 |
---|---|
SocketAddress getSocketAddress() | 获取主机IP地址和端口号 |
InetAddress getAddress() | 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址 |
int getPort() | 从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号 |
byte[] getData() | 获取数据报中的数据 |
int getLength() | 获取数据长度 |
构造UDP发送的数据报时,需要传入 SocketAddress ,该对象可以使用 InetSocketAddress 来创建。
2.3 InetSocketAddress API
InetSocketAddress ( SocketAddress 的子类 )构造方法:
方法签名 | 方法说明 |
---|---|
InetSocketAddress(InetAddress addr, int port) | 创建一个Socket地址,包含IP地址和端口号 |
2.4 回显程序 (UDP)
(服务器) 过程及代码实现:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
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) {
// 循环里面处理一次请求.
// 1. 读取请求并解析
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(requestPacket);
// 把这个 DatagramPacket 对象转成字符串, 方便去打印.
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
// 2. 根据请求计算响应
String response = process(request);
// 3. 把响应写回到客户端
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
requestPacket.getSocketAddress());
socket.send(responsePacket);
// 4. 打印一个日志, 记录当前的情况
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();
}
}
(客户端) 过程及代码实现:
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIP;
private int serverPort;
// 两个参数一会会在发送数据的时候用到.
// 暂时先把这俩参数存起来, 以备后用.
public UdpEchoClient(String serverIP, int serverPort) throws SocketException {
// 这里并不是说就没有端口, 而是让系统自动指定一个空闲的端口~~
socket = new DatagramSocket();
// 假设 serverIP 是形如 1.2.3.4 这种点分十进制的表示方式 (关于点分十进制, 后面详细说)
this.serverIP = serverIP;
this.serverPort = serverPort;
}
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
while (true) {
// 1. 从控制台读取用户输入的内容.
System.out.print("-> ");
String request = scanner.next();
// 2. 构造一个 UDP 请求, 发送给服务器.
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
InetAddress.getByName(this.serverIP), this.serverPort);
socket.send(requestPacket);
// 3. 从服务器读取 UDP 响应数据. 并解析
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
// 4. 把服务器的响应显示到控制台上.
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
client.start();
}
}
细节:
1)
可以通过 jconsole 来观察阻塞~~
2)客户端的端口号是系统分配的,而服务器的端口号是自己设置的。即为什么服务器就不害怕端口冲突,而客户端就担心端口冲突呢???
2.5 翻译程序 (UDP)
服务器代码完全可以继承上一题的啊~~
(服务器) 过程及代码实现:
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
public class UdpTranslateServer extends UdpEchoServer {
// 翻译是啥? 本质上就是 key -> value
private Map<String, String> dict = new HashMap<>();
public UdpTranslateServer(int port) throws SocketException {
super(port);
dict.put("cat", "小猫");
dict.put("dog", "小狗");
dict.put("fuck", "卧槽");
// 在这里就可以填入很多很多的内容. 像有道这样的词典程序, 也就无非如此, 只不过这里有一个非常大的哈希表, 包含了几十万个单词.
}
// 重写 process 方法, 实现查询哈希表的操作
@Override
public String process(String request) {
return dict.getOrDefault(request, "词在词典中未找到");
}
// start 方法和父类完全一样, 不用写了.
public static void main(String[] args) throws IOException {
UdpTranslateServer server = new UdpTranslateServer(9090);
server.start();
}
}
客户端代码上一题的同样适用~~
(客户端) 过程及代码实现:
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpTranslateClient {
private DatagramSocket socket = null;
private String serverIP;
private int serverPort;
// 两个参数一会会在发送数据的时候用到.
// 暂时先把这俩参数存起来, 以备后用.
public UdpTranslateClient(String serverIP, int serverPort) throws SocketException {
// 这里并不是说就没有端口, 而是让系统自动指定一个空闲的端口~~
socket = new DatagramSocket();
// 假设 serverIP 是形如 1.2.3.4 这种点分十进制的表示方式 (关于点分十进制, 后面详细说)
this.serverIP = serverIP;
this.serverPort = serverPort;
}
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
while (true) {
// 1. 从控制台读取用户输入的内容.
System.out.print("-> ");
String request = scanner.next();
// 2. 构造一个 UDP 请求, 发送给服务器.
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
InetAddress.getByName(this.serverIP), this.serverPort);
socket.send(requestPacket);
// 3. 从服务器读取 UDP 响应数据. 并解析
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
// 4. 把服务器的响应显示到控制台上.
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
client.start();
}
}
三、TCP流套接字编程
3.1 ServerSocket API
ServerSocket 是创建TCP服务端Socket的API。
ServerSocket 构造方法:
方法签名 | 方法说明 |
---|---|
ServerSocket(int port) | 创建一个服务端流套接字Socket,并绑定到指定端口 |
ServerSocket 方法:
方法签名 | 方法说明 |
---|---|
Socket accept() | 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待 |
void close() | 关闭此套接字 |
3.2 Socket API
Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。
不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。(客户端和服务端都使用)
Socket 构造方法:
方法签名 | 方法说明 |
---|---|
Socket(String host, int port) | 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接 |
Socket 方法:
方法签名 | 方法说明 |
---|---|
InetAddress getInetAddress() | 返回套接字所连接的地址 |
InputStream getInputStream() | 返回此套接字的输入流 |
OutputStream getOutputStream() | 返回此套接字的输出流 |
3.3 回显程序 (TCP)
(服务器) 过程及代码实现:
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;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TcpEchoServer {
// 代码中会涉及到多个 socket 对象. 使用不同的名字来区分.
private ServerSocket listenSocket = null;
public TcpEchoServer(int port) throws IOException {
listenSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
ExecutorService service = Executors.newCachedThreadPool();
while (true) {
// 1. 先调用 accept 来接受客户端的连接.
Socket clientSocket = listenSocket.accept();
// 2. 再处理这个连接. 这里应该要使用多线程. 每个客户端连上来都分配一个新的线程负责处理~
// 此处使用多线程, 雀食能解决问题, 但是会导致频繁创建销毁多次线程!!
// Thread t = new Thread(() -> {
// try {
// processConnection(clientSocket);
// } catch (IOException e) {
// e.printStackTrace();
// }
// });
// t.start();
service.submit(new Runnable() {
@Override
public void run() {
try {
processConnection(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
private void processConnection(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
// 接下来就处理客户端的请求了.
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
while (true) {
// 1. 读取请求并解析.
Scanner scanner = new Scanner(inputStream);
if (!scanner.hasNext()) {
// 读完了, 连接可以断开了.
System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress().toString(),
clientSocket.getPort());
break;
}
String request = scanner.next();
// 2. 根据请求计算响应
String response = process(request);
// 3. 把响应写回给客户端
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 {
// 为啥这个地方要关闭 socket ? 而前面的 listenSocket 以及 udp 程序中的 socket 为啥就没 close??
clientSocket.close();
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
细节:
1)
2)为什么这里需要clientSocket.close();
,而前面的 listenSocket 以及 UDP 程序中的 socket 为啥就不必 close?
3)若不使用多线程处理 clientSocket,则会在等待用户输入时进行阻塞,进而无法同时为多个客户端提供服务!!!
那么为什么相比于上述UDP代码,TCP需要特别使用多线程呢?
1.使用TCP需要先建立一对一的客户端服务器连接;
2.TCP建立连接之后需要处理客户端的多次请求 (长连接),导致无法快速调用到 accept;如果TCP每个连接只处理一个客户端的请求 (短连接),也能够保证快速调用到 accept!!!修改循环即可~~
4)这里会多次频繁创建销毁线程,因此使用线程池最为合适!!!
(客户端) 过程及代码实现:
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 {
// 和服务器建立连接. 就需要知道服务器在哪了.
// 这里和上节课写的 UDP 客户端差别较大了.
socket = new Socket(serverIP, serverPort);
}
public void start() {
Scanner scanner = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
while (true) {
// 1. 从控制台读取数据, 构造成一个 请求
System.out.print("-> ");
String request = scanner.next();
// 2. 发送请求给服务器
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
// 这个 flush 不要忘记. 否则可能导致请求没有真发出去.
printWriter.flush();
// 3. 从服务器读取响应
Scanner respScanner = new Scanner(inputStream);
String response = respScanner.next();
// 4. 把响应显示到界面上
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();
}
}
3.4 翻译程序 (TCP)
服务器代码完全可以继承上一题的啊~~
(服务器) 过程及代码实现:
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class TcpTranslateServer extends TcpEchoServer{
private Map<String, String> dict = new HashMap<>();
public TcpTranslateServer(int port) throws IOException {
super(port);
dict.put("cat", "小猫");
dict.put("dog", "小狗");
dict.put("fuck", "卧槽");
}
@Override
public String process(String request) {
return dict.getOrDefault(request,"词在词典中未找到");
}
public static void main(String[] args) throws IOException {
TcpTranslateServer server = new TcpTranslateServer(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 TcpTranslateClient {
// 客户端需要使用这个 socket 对象来建立连接.
private Socket socket = null;
public TcpTranslateClient(String serverIP, int serverPort) throws IOException {
// 和服务器建立连接. 就需要知道服务器在哪了.
// 这里和上节课写的 UDP 客户端差别较大了.
socket = new Socket(serverIP, serverPort);
}
public void start() {
Scanner scanner = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
while (true) {
// 1. 从控制台读取数据, 构造成一个 请求
System.out.print("-> ");
String request = scanner.next();
// 2. 发送请求给服务器
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
// 这个 flush 不要忘记. 否则可能导致请求没有真发出去.
printWriter.flush();
// 3. 从服务器读取响应
Scanner respScanner = new Scanner(inputStream);
String response = respScanner.next();
// 4. 把响应显示到界面上
System.out.println(response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpTranslateClient client = new TcpTranslateClient("127.0.0.1", 9090);
client.start();
}
}