一、UDP Socket编程
由于UDP是面向数据报的,我们需要用一个类来表示数据包,即DatagarmPacket.
DatagramPacket API
构造方法:
方法名 | 方法说明 |
---|---|
DatagramPacket(byte[] buf, int length) | 构造一个DatagramPacket来接收数据报,数据存储在buf数组中,长度为length. |
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) | 构造一个DatagramPacket来发送数据报,发送的数据是从buf数组的offset位置开始往后length长度的数据,address指定目的ip和port |
成员方法:
方法名 | 方法说明 |
---|---|
SocketAddress getSocketAddress() | 返回SocketAddress,一般是ip+port |
int getPort() | 返回port |
InetAddress getAddress() | 返回ip地址 |
byte[] getData() | 获取数据包中的数据 |
DatagramSocket API
我们为了是层间传输的数据更小,就引入了Socket,我们需要使用Socket来表示一些特定的信息.如UDP的Socket本质上是一个整数,用来代表的一端的会话关系。
构造方法:
方法名 | 方法说明 |
---|---|
DatagramSocket() | 一般是在客户端使用,创建一个UDP的Socket,端口号由系统分配 |
DatagramSocket(int port) | 一般是在服务器端使用,创建一个UDP的Socket,端口号手动分配 |
成员方法:
方法名 | 方法说明 |
---|---|
void receive(DatagramPacket p) | 此处的p是返回型参数,将收到的数据报放到p中,如果没有接受到,会阻塞等待 |
void send(DatagramPacket p) | 将数据报p发送 |
大致流程
简单通信程序
在这里我们实现一个简单的UDP客户端/服务器通信程序,这个程序中没啥业务逻辑,是一个回显服务器,服务器收到客户端的字符串后,原封不动的返回即可。
UDPEchoServer
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);
}
//通过start方法来启动服务器
public void start() throws IOException {
System.out.println("服务器启动!");
//服务器一般都是7*24小时运行着
while(true){
//1.读取请求并解析
DatagramPacket requestPacket = new DatagramPacket(new byte[1024], 1024);
//服务器没收到请求的话就会在这阻塞等待
socket.receive(requestPacket);
//2.根据请求计算响应
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
String response = process(request);
//3.将响应发送给客户端
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), 0, response.getBytes().length, requestPacket.getAddress(), requestPacket.getPort());
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(8080);
server.start();
}
}
UdpEchoClient
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 ip, int port) throws SocketException {
serverIp = ip;
serverPort = port;
socket = new DatagramSocket();
}
public void start() throws IOException {
System.out.println("客户端上线!");
//循环去发请求
while(true){
Scanner sc = new Scanner(System.in);
System.out.println("请输入:");
String request = sc.next();
//1.将服务器的地址和端口号放进数据报,然后发送
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), 0, request.getBytes().length, InetAddress.getByName(serverIp), serverPort);
socket.send(requestPacket);
//2.等待接受服务器发来的响应
DatagramPacket responsePacket = new DatagramPacket(new byte[1024], 1024);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(), 0, responsePacket.getData().length);
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
new UdpEchoClient("127.0.0.1", 8080).start();
}
}
二、TCP Socket编程
由于Tcp是面向字节流和有连接的,因此与Udp编程有一些差异,不过依旧是那三部曲:服务器读取请求并解析,根据请求计算响应,将响应返回给客户端。
Tcp的socket是用来标识通信的双方,是一个四元组,包含了:源ip、源port、目的ip、目的port。
ServerSocket API
ServerSocket是在服务器端使用的。
构造方法:
方法名 | 方法说明 |
---|---|
ServerSocket(int port) | 创建一个服务器端的socket,并绑定到指定端口 |
普通方法:
方法名 | 方法说明 |
---|---|
Socket accept() | 守候自己的端口号上等待用户的连接,当有连接后,返回一个Socket对象,表示与客户端建立的连接。 |
void close() | 关闭socket |
Socket API
这个socket类可以表示客户端Socket,也可以表示为服务器端接受到客户端连接的请求后,返回的服务器端Socket。
构造方法:
方法名 | 方法说明 |
---|---|
Socket(String host, int port) | 创建一个socket,与对应ip上的对应port建立连接 |
普通方法:
方法名 | 方法说明 |
---|---|
InetAddress getInetAddress() | 返回socket所连接的地址 |
InputStream getInputStream() | 返回socket的输入流 |
OutputStream getOutputStream() | 返回socket的输出流 |
大致流程
简单通信程序
TcpEchoServer:
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 serverSocket = null;
//指定服务器端的port
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
//启动服务器
public void start() throws IOException {
System.out.println("服务器启动!");
//服务器往往是7*24小时一直运转~
while(true){
//获取一个和服务器端的连接
Socket socket = serverSocket.accept();
//处理连接
processConnection(socket);
}
}
public void processConnection(Socket socket){
System.out.printf("[%s:%d] 客户端已经上线~\n", socket.getInetAddress().toString(), socket.getPort());
//与udp同理三部曲,不过tcp是字节流,使用流对象来读请求和发响应
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
//为了方便,使用Scanner直接当做字符来处理
Scanner scanner = new Scanner(inputStream);
//客户端可能会多次发请求
while(true){
if(!scanner.hasNext()){
System.out.printf("[%s:%d] 客户端已经下线~\n", socket.getInetAddress().toString(), socket.getPort());
break;
}
//1、读取请求并解析
String request = scanner.next();
//2.根据请求计算响应
String response = process(request);
//3.将响应发送给客户端
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();
}
}
public String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(8080);
server.start();
}
}
TcpEchoClient:
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 ip, int port) throws IOException {
//指定服务器的地址和端口号
socket = new Socket(ip, port);
}
public void start(){
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
Scanner scannerConsole = new Scanner(System.in);
while(true){
//1.组织请求
String request = scannerConsole.next();
//2.将请求发送给服务器
PrintWriter writer = new PrintWriter(outputStream);
writer.println(request);
writer.flush();
//3.接受服务器端的响应
Scanner scannerResponse = new Scanner(inputStream);
String response = scannerResponse.next();
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", 8080);
client.start();
}
}
关闭连接
虽然Java有着垃圾回收机制,但是我们的服务器端一般是7*24小时运行着,而我们的代码中有着一个死循环,它一直在创建Socket对象,而每个socket对象与文件有关,也就是说每创建一个socket对象会占用一定的文件资源,因此我们需要去手动释放它。由于Socket类实现了closeable接口,我们可以使用try with resourse来关闭。
那么我们是否还需要去手动关闭Scanner和PrintWriter呢?不用,这是因为这两个里面持有的是inputStream和outputStream对象的引用,我们已经设置了自动关闭这两个对象,因此我不需要再关闭一次。
多个客户端访问
虽然我们代码现在是安全了,但是还存在一个问题,当我们来了多个客户端访问服务器的时候,由于我们在processConnection中写了一个while循环,那么当客户端A进来后,程序就会阻塞在这个循环中,直到客户端A断开连接后,服务器才能去服务器客户端B,因此我们需要使用多线程来处理并发。
很自然的我们可以写出这样的代码,然后就会出现如下错误:
这是因为我们主线程在执行完代码块的时候,我们的另一个线程去执行了processConnction方法,但是还没执行完,我们的主线程就自动调用了socket.close,于是就抛出了上述异常。
正确写法应该这样:
在processConnection中关闭连接,因为一个processConnection处理完了,也就相当于一个连接结束了,因此我们可以在这进行关闭。
不过手动创建线程会涉及到用户态和内核态的转变,我们还可以使用一个线程池来进一步优化效率。
最终结果如下: