网络编程套接字
socket,是操作系统给应用程序(传输层给应用层)提供的api,Java也对这个api进行了封装。
socket提供了两组不同的api,UDP有一套,TCP有一套,本文主要介绍api的使用
TCP、UDP的区别:
TCP:有连接、可靠传输、面向字节流、全双工
UDP:无连接、不可靠传输、面向数据报、全双工
有连接和无连接:通信双方保存了通信对方的信息就是有连接,如果不保存就是无连接。
可靠和不可靠:可靠就是确保数据尽可能到达对方,而不可靠就是完全不考虑数据是否能够到达对方。
面向字节流和面向数据报:字节流和文件流的特点是一样的;面向数据报是传输数据报,而不是单个字节。
全双工:通信双方都可以发送和接收数据,双向通信。
UDP、TCP的api使用
UDP的api
DatagramSocket、DatagramPacket
DatagramSocket代表一个Socket对象,操作系统把网卡这种硬件设备的概念封装成socket,操作socket对象就能间接操控网卡,相当于网卡的遥控器。
DatagramPacket代表一个UDP数据报
网络编程需要操作网卡,需要用到socket对象
DatagramSocket:
构造方法:
方法名 | 说明 |
---|---|
DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端) |
DatagramSocket(int port) | 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(port就是端口号) |
方法:
方法名 | 说明 |
---|---|
void receive(DatagramPacket p) | 从该套接字接收数据报(如果没有接收到数据报,该方法会阻塞) |
void send(DatagramPacket p) | 从该套接字发送数据包(不会阻塞等待) |
void close() | 关闭数据报套接字 |
DatagramPacket:
构造方法:
方法名 | 说明 |
---|---|
DatagramPacket(byte[] buf,int length) | 构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组buf中,接收指定的长度(length) |
DatagramPacket(byte[] buf,int offset,int length,SocketAddress address) | 构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组buf中,接收指定的长度(length),address表示指定的目的主机的ip和端口号 |
方法:
方法名 | 说明 |
---|---|
InetAddress getAddress() | 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址 |
int getPort() | 从接收的数据报中,获取发送端主机的端口号,或者从发送的数据报中,获取接收端主机的端口号 |
byte[] getData() | 获取数据报中的数据 |
构造UDP发送的数据报,需要传SocketAddress对象,可以用InetSocketAddress来创建这个对象
InetSocketAddress:
方法 | 说明 |
---|---|
InetSocketAddress(InetAddress addr,int port) | 创建一个Socket地址,包含IP地址和端口号 |
实现一个最简单的UDP客户端服务器程序,不涉及业务流程用于演示api做法,
回显服务器(echo server):客户端发什么请求,服务器就返回什么响应,没有任何业务逻辑
服务端代码:
java
public class udpEchoServer {
private DatagramSocket socket = null;
public udpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);//对于服务器一端来说,创建socket对象需要指定端口号作为参数,后续服务器运作之后,操作系统会把端口号和进程关联起来
//调用构造方法的过程中,JVM会调用系统的socket api,完成端口号-进程 之间的关联关系,这样的操作也叫绑定端口号
//对于一个系统来说,同一个协议下,同一时刻一个端口号只能被一个进程绑定,但是一个进程可以绑定多个端口号(通过创建多个socket对象来完成),端口号就是为了区分进程
}
//启动服务器,服务器的工作是处理客户端发来的请求
//请求(request):客户端主动给服务器发起的数据
//响应(response):服务器给客户端返回的数据
public void start() throws IOException {
System.out.println("服务器启动");
while (true) {
//处理请求
//服务器一般分为3个步骤
//1.从网卡中读取客户端的请并且解析求
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(requestPacket);
//receive的参数是DatagramPacket对象,需要构造一个DatagramPacket对象,而构造DatagramPacket对象需要传入一个字节数组和数组长度,长度要保证能够存储数据包
//从网卡收到的数据(是二进制数据)也会写入到数组中
//如果网卡上收到数据,receive立即返回,并且获取到数据
//如果网卡上没有数据,receive会阻塞等待,直到收到了数据为止
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());
//第一个参数是获取字节数组(因为创建String对象的时候就是使用数组的方式),
//第二个参数是这个数组的长度,不是字符串的长度!!
//第三个参数是客户端的信息(IP+端口号),因为UDP是无连接的,不保存双方的信息,在进行send时我们要自己把这些信息写进数据包中
socket.send(responsePacket);
//4.打印日志
System.out.printf("[%s:%d] req=%s,resp=%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();
}
}
客户端代码:
java
public class udpEchoClient {
DatagramSocket socket = null;
private String serverIp;//服务器的ip
private int serverPort;//服务器的端口号
public udpEchoClient(String serverIp, int serverPort) throws SocketException {
socket = new DatagramSocket();
//客户端一般不指定端口号,客户端是主动的一方,不需要让服务器找他.让系统自动分配端口
//用户的电脑上运行了很多程序,用户可能指定的端口号是和其他程序的端口冲突了
this.serverIp = serverIp;
this.serverPort = serverPort;
}
public void start() throws IOException {
System.out.println("启动客户端");
Scanner scanner = new Scanner(System.in);
while (true) {
//1.从控制台读取到用户的输入
String request = scanner.nextLine();
//2.构造UDP请求,发送给服务器
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length
, InetAddress.getByName(this.serverIp), this.serverPort);//
//InetAddress.getByName(this.serverIp),这里是把ip包裹一下,转换成所需的二进制格式,
socket.send(requestPacket);
//3.从服务器中读取响应
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
//4.把响应打印到看看系统上
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();
}
}
服务器的基本流程:
1、读取请求并解析
2、根据请求计算响应
3、响应写回客户端
客户端的基本流程:
1、从控制台读取数据
2、构造请求,发送给服务器
3、从服务器读取响应
4、显示结果到控制台
在Windows上使用如下命令,可以查看主机上的网络相关信息
cmd
netstat -ano
使用如下命令,可以查找指定端口号的网络信息
cmd
netstat -ano findstr 端口号
TCP的api
核心的两个类:ServerSocket、Socket,ServerSocket是专门给服务器使用的socket对象;Socket既会给客户端使用,也会给服务器使用
TCP是面向字节流的,传输数据的基本单位就是字节byte
ServerSocket的构造方法:
方法名 | 说明 |
---|---|
ServerSocket(int port) | 创建一个服务端流套接字Socket,并且绑定到指定端口 |
ServerSocket的方法:
建立连接的过程类似打电话,accept相当于接电话
方法名 | 说明 |
---|---|
Socket accept() | 开始监听指定端口,有客户端连接后,返回一个服务端Socket对象,并且基于Socket对象建立与客户端的连接,如果没有连接则阻塞等待 |
void close() | 关闭套接字 |
Socket的构造方法:
构造Socket对象,就是和服务器打电话,建立连接
方法名 | 说明 |
---|---|
Socket(String host,int port) | 创建一个客户端流套接字Socket,并且与对应IP的主机上对应的端口的进程建立连接 |
Socket的方法:
方法 | 说明 |
---|---|
InetAddress getInetAddress() | 返回套接字所连接的地址(得到对端的信息) |
InputStream getInputStream() | 返回此套接字的输入流 |
OutputStream getOutputStream() | 返回此套接字的输出流 |
实现一个回响服务器:
服务器端:
java
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();//接电话,
//Socket对象用于和客户一对一服务,ServerSocket用于揽客
Thread t = new Thread(()->{
try {
processConnection(clientSocket);//针对一个连接,使用多线程来提供处理逻辑
//processConnection代码中也有一个while循环,进入里层循环就不能执行外层循环了,此时当有多个客户端发来请求,只能处理一个,使用多线程解决这个问题
} catch (IOException e) {
e.printStackTrace();
}
});
//这里的Socket对象:clientSocket是连接级别的数据,需要主动close
// 客户端断开连接之和就要close
t.start();
// 使用线程池
// ExecutorService service = Executors.newCachedThreadPool();
// service.submit(()->{
// try {
// processConnection(clientSocket);
// } catch (IOException e) {
// e.printStackTrace();
// }
// });
}
}
private void processConnection(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d] 客户端上线\n", clientSocket.getInetAddress(), clientSocket.getPort());
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
Scanner scanner = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
while (true) {
//1.读取请求并且解析
if (!scanner.hasNext()) {
//scanner中无法读取数据,说明客户端关闭了连接,导致服务器端读取到末尾
break;
}
//2.根据请求计算响应
String request = scanner.next();
String response = process(request);
//3.把响应写回客户端
// outputStream.write(response.getBytes());
printWriter.println(response);
printWriter.flush();//刷新缓冲区
//4.打印日志
System.out.printf("[%s:%d] req=%s;rep=%s\n", clientSocket.getInetAddress(), clientSocket.getPort()
, request, response);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
clientSocket.close();
}
System.out.printf("[%s:%d] 客户端下线\n", clientSocket.getInetAddress(), clientSocket.getPort());
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TCPEchoServer server = new TCPEchoServer(9090);
server.start();
}
}
客户端:
java
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("客户端启动");
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner scanner = new Scanner(inputStream);
Scanner in = new Scanner(System.in);
PrintWriter printWriter = new PrintWriter(outputStream);
while (true) {
//1.从控制台读取
System.out.println("->");
String request = in.next();
//2.把请求发送给服务器
printWriter.println(request);
printWriter.flush();
//printWriter这样的类,自带缓冲区,写入数据不会立即触发IO,而是先放入缓冲区中,等缓冲区攒了一波之和,再统一性发送
//使用flush可以主动刷新缓冲区
//3.从服务器读取响应
if (!scanner.hasNext()) {
break;
}
String response = scanner.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();
}
}
短时间内有大量客户:
1、客户端发送完一个请求之后很快就断开连接了:使用线程池
2、客户端持续的发送请求处理响应,连接会保持很久:IO多路复用
长连接和短连接:
长连接:客户端连上服务器之后,一个连接中会发起多次请求,接受多个响应
短连接:客户端连上服务器之后,一个连接中会发起一次请求,接受一个响应