服务器端:
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
// 绑定地址
private static String ADDRESS = "127.0.0.1";
// 监听端口号
private static final int PORT = 8080;
public static void main(String[] args) throws IOException {
// 创建 server 对象
Server server = new Server();
// 调用 service() 方法,等待请求
server.service();
}
public void service() {
// 初始化
ServerSocket server = null;
try {
// 创建 InetAddress 对象简单的方法是调用其静态方法 getByName()
InetAddress adr = InetAddress.getByName(ADDRESS);
// 创建 ServerSocket 对象
// 参数说明:1.监听端口号 2.请求最大队列长度 3.绑定地址
server = new ServerSocket(PORT, 5, adr);
System.out.println("服务启动成功");
} catch (IOException e) {
System.out.println("服务启动失败");
e.printStackTrace();
}
// 循环等待请求
while (true) {
// 初始化参数
Socket socket = null;
try {
// accept 方法会从连接请求队列中取出一个连接请求,然后创建与客户端连接的 Socket 对象,并将它返回
// 如果队列中没有连接请求,accept() 方法就会一直等待,直到接收到了连接请求才返回
socket = server.accept();
// 打印请求地址和端口
System.out.println("连接来自:" + socket.getLocalSocketAddress());
// 解析客户端发来的请求
String request = parse(socket.getInputStream());
// 对请求进行处理
String response = process(request);
// 将请求结果发送给客户端
send(socket.getOutputStream(), response);
// 关闭当前的 socket
socket.close();
} catch (Exception e) {
e.printStackTrace();
// 出错时重新开始新的循环
continue;
}
}
}
// 返回值可以认为是一个 Request 的对象
protected String parse(InputStream input) {
int len;
String charset = "UTF-8";
byte[] buffer = new byte[2048];
try {
// 一次读取全部的请求信息(因为请求一般不会太长)
len = input.read(buffer);
// 解析处理(暂无)
// TODO
// 解析结果返回
return new String(buffer, 0, len, charset);
} catch (IOException e) {
e.printStackTrace();
return "解析请求时发生异常!";
}
}
// 对请求进行处理,并返回处理结果
protected String process(String request) {
// 这里只做了简单的处理
return "来自服务端的消息:\r\n" + request;
}
// 这里的第二个参数,可以理解为 Response 对象
protected void send(OutputStream output, String response) {
// 创建 PrintWriter 对象,第二个参数为 true,则 println、printf 或 format 方法将刷新输出缓冲区
PrintWriter out = new PrintWriter(output, true);
// 将 response 发送给客户端
out.println(response);
}
}
客户端:
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class Client {
public static void main(String[] args) {
// 连接到的主机地址
String host = "127.0.0.1";
// 连接的端口号
int port = 8080;
try {
// 创建连接服务器的 Socket 对象
Socket socket = new Socket(host, port);
// 要从连接的另一端接受字节流,需要调用 Socket 类的 getInputStream 方法获取 InputStream 对象,
// 之后可以将其作为参数创建需要的 BufferedReader 对象
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// 需要发送字节流时,需要调用 Socket 类的 getOutputStream 方法获取 OutputStream 对象,
// 之后可以将其用来创建需要的 PrintWriter 对象
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
// 向服务器端发送请求
out.println("Hello World!");
// 接受服务器端发送回来的信息
while (true) {
// ready() 方法判断此数据流是否已准备好被读取,这里主要是判断缓冲区是否为空
if (in.ready()) {
// 缓冲区不为空时,按行读取流中数据
String line;
while ((line = in.readLine()) != null) {
System.out.println(line);
}
// 读取结束后跳出循环
break;
}
// 缓冲区为空,则线程等待 50 毫秒(等待服务器发送数据)
Thread.sleep(50);
}
// 关闭 socket
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
知识点补充:
1. 构造 ServerSocket
ServerSocket 构造方法有以下四种形式:
- ServerSocket()throws IOException
- ServerSocket(int port) throws IOException
- ServerSocket(int port, int backlog) throws IOException
- ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException
在以上构造方法中,参数 port 指定服务器要绑定的端口(监听端口),参数 backlog 指定客户端连接请求队列的长度,参数 bindAddr 指定服务端要绑定的 IP 地址。
1.1 绑定端口
除了第一个不带参数的构造方法之外,其它构造方法都会使服务器和特定端口绑定,端口由参数 port 指定,当运行时无法绑定到某端口时,会抛出 IOException, 更确切的说是 BindException,它是 IOException 的子类。BindException 一般有以下几个原因造成:
a)端口已经被其它服务器进程占用
b)在某些操作系统中,如果没有以超级用户的身份来运行服务器程序,那么操作系统不允许服务器绑定到 1~1023 端口
如果把参数 port 设为 0,表示由操作系统分配任意一个可用的端口。由服务器分配的端口也称之为匿名端口。对于多数服务器,会使用明确的端口,因为客户程序需要事先知道服务器端口,才能方便的访问服务器。在某些场合才会使用到匿名端口,比如 FTP(文件传输)协议等。
1.2 设定客户连接请求队列的长度
当服务器进程运行时,可能会同时监听到多个客户的连接请求。管理这些请求的任务是由操作系统来完成的,操作系统会把这些连接请求存储在一个先进先出的队列中。很多系统限定了队列的最大长度为 50。当队列中的请求达到了队列的最大容量时,服务器进程所在的主机就会拒绝新的连接请求。只有当服务器进程通过 ServerSocket 的accep() 方法从队列中取出连接请求,使队列腾出空位时,队列才能继续加入新的连接请求。
对于客户端进程,如果它发出的连接请求被加入到服务器的队列中,就意味着客户和服务器的连接建立成功,客户进程从 Socket 构造方法中正常返回。
ServerSocket 构造方法的 backlog 参数用来显示设置连接请求队列的长度,它将覆盖系统设定的最大长度,但在以下情况,仍然会采用系统限定的队列的最大长度:
a)backlog 参数的值大于系统限定的队列的最大长度
b)backlog 参数的值小于或等于 0
c)在 ServerSocket 构造函数中没有设置 backlog 参数
1.3 设定绑定的 IP 地址
如果主机只有一个 IP 地址,那么默认情况下,服务器就会与该 IP 地址绑定。ServerSocket 4个构造方法中只有第四个显示指定服务器要绑定的 IP 地址,该构造方法适用于具有多个 IP 地址的主机。
1.4 默认构造方法的作用
ServerSocket 有一个不带任何参数的默认构造方法,通过该构造方法创建的 ServerSocket 不和任何端口绑定,但是之后需要通过 bind() 方法与特定端口绑定。这么做的目的是,允许服务器在绑定到特定端口之前,先设置 ServerSocket 的一些选项。因为一点服务器与特定端口绑定,有些选项就不能在改变了。
例如:
ServerSocket serverSocket=new ServerSocket();
serverSocket.setReuseAddress(true); // 设置 ServerSocket 选项
serverSocket.bind(new InetSocketAddress(8080)); // 与8080端口绑定
如果把 2、3 句代码互换,那么设置 ServerSocket 选项就完全不起作用了。
ServerSocket 有以下 3 个选项:
- SO_TIMEOUT:表示等待客户连接的超时时间
- SO_REUSEADDR:表示是否允许重用服务器所绑定的地址
- SO_RCVBUF:表示接收数据的缓冲区的大小