文章目录
1. 网络编程基础
1.1 为什么需要网络编程?
这是因为在我们本地资源中,里面的内容并不丰富,而且下载的资源大多最终还是来源于网络。
而在网络世界中,里面有大量的资源。例如:当我们在网页中打开一个视频,听一段音乐,看一篇文章等,这些都属于网络资源(能从网络中获得的数据资源)。
这些资源都是通过网络编程进行数据传输,所以网络编程的需要是必不可少的。
1.2 网络编程是什么?
网络编程就是指网络主机通过不同的进程,以编程的方式进行网络通信(或称网络数据传输)。
1.3 概念
- 发送端:数据发送方的进程,称发送端。发送端的主机即网络通信中的源主机。
- 接收端:数据接受方的进程,称接受端。接受端的主机即网络通信中的目的主机。
- 收发端:发送端和接受端简称收发端。
注意:
发送端和接收端只是相对的,只是一次网络数据传输产生数据流向后的概念。
如上图:进程1请求获得资源时,进程1就是发送端,进程2是接收端,进程2响应时,进程2就是发送端,进程1就是接收端。- 服务端:提供服务的一端,称为服务端。
- 客户端:获取服务的一端,称为客户端。
如上图:主机1发出请求,主机2做出响应,那么我们就可以称主机1为客户端,主机2为服务端。
2. Socket套接字
概念:
Socket套接字,是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。基于Socket套接字的网络程序开发就是网络编程。
分类:
- 流套接字:使用传输层TCP协议。
TCP的特点:
有连接
可靠传输
面向字节流
有接收缓冲区,也有发送缓冲区
大小不限- 数据报套接字:使用传输层UDP协议
UDP的特点:
无连接
不可靠传输
面向数据报
有接收缓冲区,无发送缓冲区
大小受限:一次最多传输64k- 原始套接字:原始套接字用于自定义传输层协议,用于读写内核没有处理的IP协议数据。
3. UDP数据报套接字编程
3.1 DatagramSocket API
DatagramSocket 是UDP Socket,用于发送和接收UDP数据报。
DatagramSocket 构造方法:
方法 | 作用 |
---|---|
DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端) |
DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务端) |
//创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口
DatagramSocket socket1 = new DatagramSocket();
//创建一个UDP数据报套接字的Socket,绑定到本机9090端口
DatagramSocket socket2 = new DatagramSocket(9090);
DatagramSocket 方法:
方法 | 作用 |
---|---|
void receive(DatagramPacket p) | 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) |
void send(DatagramPacketp) | 从此套接字发送数据报包(不会阻塞等待,直接发送) |
void close() | 关闭此数据报套接字 |
3.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 方法:
方法 | 作用 |
---|---|
InetAddress getAddress() | 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址 |
int getPort() | 从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号 |
byte[] getData() | 获取数据报中的数据 |
3.3 InetSocketAddress API
构造UDP发送的数据报时,需要传入 SocketAddress ,该对象可以使用 InetSocketAddress 来创建。
InetSocketAddress ( SocketAddress 的子类 )构造方法:
方法 | 作用 |
---|---|
InetSocketAddress(InetAddress addr, int port) | 创建一个Socket地址,包含IP地址和端口号 |
4. UDP构建服务端客户端(一发一收)
知道上面的,就可以创建一个简单的服务端客户端,一发一收,就是客户端请求服务端,请求什么,客户端收到什么。
4.1 创建服务端UdpServer.java:
public class UdpServer {
//创建服务器socket,不实例化
private DatagramSocket socket = null;
public UdpServer(int port) throws SocketException {
服务器socket绑定固定的端口
socket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
//服务器放入死循环中,我们并不知道客户端什么时候访问客户端,需要不停的接受客户端的UDP数据报
while(true){
//1.创建数据报,读取客户端发送数据
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
socket.receive(requestPacket);
//转义
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);
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 {
//实例化服务端,端口号为9092
UdpServer udpServer = new UdpServer(9092);
//启动服务端
udpServer.start();
}
}
4.2 创建客户端UdpClient.java:
public class UdpClient {
private DatagramSocket socket = null;
private String serverIp;
private int serverPort;
public UdpClient(String ip, int port) throws SocketException {
serverIp = ip;
serverPort = port;
socket = new DatagramSocket();
}
public void start() throws IOException {
System.out.println("客户端启动");
Scanner sc = new Scanner(System.in);
while(true){
//1.输入
System.out.println("-> ");
String request = sc.next();
//2.发送
// InetSocketAddress inetAddress = new InetSocketAddress(serverIp,serverPort);
// DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
// inetAddress);
// 计算机内部处理数据的基本单位是字节。通过将接收到的数据转义为字节,长度要用字节长度
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(serverIp),serverPort);
socket.send(requestPacket);
//3.读取
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],0,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 {
UdpClient udpClient = new UdpClient("127.0.0.1",9092);
udpClient.start();
}
}
运行结果:
5. UDP构建服务端客户端(翻译)
4中服务器并没有什么功能,那么我们也可以为服务器赋予一些功能,例如:翻译。
我们只需要基于4代码的基础上,对服务器进行修改。
创建翻译服务端UdpServer.java:
public class UdpServer {
//创建服务器socket,不实例化
private DatagramSocket socket = null;
//翻译集合
private Map<String,String> dict = new HashMap<>();
public UdpServer(int port) throws SocketException {
//服务器socket绑定固定的端口
socket = new DatagramSocket(port);
//添加单词
dict.put("cat","小猫");
dict.put("dog","小狗");
dict.put("bird","鸟");
}
public void start() throws IOException {
System.out.println("服务器启动!");
//服务器放入死循环中,我们并不知道客户端什么时候访问客户端,需要不停的接受客户端的UDP数据报
while(true){
//1.创建数据报,读取客户端发送数据
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
socket.receive(requestPacket);
//转义,计算机内部处理数据的基本单位是字节。通过将接收到的数据转义为字节,服务器可以更容易地处理和解析数据,不会出错
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);
System.out.printf("[%s:%d] req: %s, resp: %s\n",requestPacket.getAddress().toString(),requestPacket.getPort(),
request,response);
}
}
//一发一收
public String process(String request){
return dict.getOrDefault(request,"我还是个小菜鸡");
}
public static void main(String[] args) throws IOException {
//实例化服务端,端口号为9092
UdpServer udpServer = new UdpServer(9092);
//启动服务端
udpServer.start();
}
}
运行结果:
其实,相对于4只是修改了一些方法和属性,那么,我们也可以重新写一个类继承4中的服务器,这样会更加的简单。
6. TCP流套接字编程
ServerSocket 是创建TCP服务端Socket的API。
6.1 ServerSocket API
ServerSocket 构造方法:
方法 | 作用 |
---|---|
ServerSocket(int port) | 创建一个服务端流套接字Socket,并绑定到指定端口 |
ServerSocket 方法:
方法 | 作用 |
---|---|
Socket accept() | 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待 |
void close() | 关闭此套接字 |
6.2 Socket API
Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。
不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。
Socket 构造方法:
方法 | 作用 |
---|---|
Socket(String host, intport) | 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接 |
Socket 方法:
方法 | 作用 |
---|---|
InetAddress getInetAddress() | 返回套接字所连接的地址 |
InputStream getInputStream() | 返回此套接字的输入流 |
OutputStream getOutputStream() | 返回此套接字的输出流 |
6.3 TCP中的长短连接
TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接:
- 短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能一次收发数据。
- 长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可以多次收发数据。
对比以上长短连接,两者区别如下:
- 建立连接、关闭连接的耗时:短连接每次请求、响应都需要建立连接,关闭连接;而长连接只需要第一次建立连接,之后的请求、响应都可以直接传输。相对来说建立连接,关闭连接也是要耗时的,长连接效率更高。
- 主动发送请求不同:短连接一般是客户端主动向服务端发送请求;而长连接可以是客户端主动发送请求,也可以是服务端主动发。
- 两者的使用场景有不同:短连接适用于客户端请求频率不高的场景,如浏览网页等。长连接适用于客户端与服务端通信频繁的场景,如聊天室,实时游戏等。
7. TCP构建服务端客户端(一发一收)
7.1 创建服务端TcpServer.java
public class TcpServer {
private ServerSocket serverSocket = null;
private ExecutorService service = Executors.newCachedThreadPool();
public TcpServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
while(true){
Socket socket = serverSocket.accept();
service.submit(new Runnable() {
@Override
public void run() {
processConnection(socket);
}
});
// Thread thread = new Thread(() -> {
// processConnection(socket);
// });
// thread.start();
}
}
public void processConnection(Socket socket){
System.out.printf("客户端上线: [%s:%d]\n",socket.getInetAddress().toString(),socket.getPort());
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
while(true){
Scanner sc = new Scanner(inputStream);
if(!sc.hasNext()){
System.out.printf("客户端下线: [%s:%d]\n",socket.getInetAddress().toString(),socket.getPort());
break;
}
String request = sc.next();
String response = process(request);
PrintWriter writer = new PrintWriter(outputStream);
writer.println(response);
writer.flush();
System.out.printf("[%s:%d] req: %s, resp: %s\n",socket.getInetAddress().toString(),socket.getPort(),
request,response);
}
}catch (IOException e){
e.printStackTrace();
}finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpServer tcpServer = new TcpServer(9090);
tcpServer.start();
}
}
7.2 创建客户端TcpClient.java
public class TcpClient {
private Socket socket = null;
public TcpClient(String ip, int port) throws IOException {
socket = new Socket(ip,port);
}
public void start(){
System.out.println("客服端启动!");
Scanner sc = new Scanner(System.in);
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
while(true){
System.out.println("-> ");
String request = sc.next();
PrintWriter writer = new PrintWriter(outputStream);
writer.println(request);
writer.flush();
Scanner scanner = new Scanner(inputStream);
String response = scanner.next();
System.out.println(response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpClient tcpClient = new TcpClient("127.0.0.1",9090);
tcpClient.start();
}
}
Tcp简单翻译和Udp修改规则相同。
8. 启动多个客户端
1.
2.
3.
4.