一、TCP流套接字编程
API介绍:
1.1 ServerSocket
ServerSocket 是创建TCP服务端Socket的API
构造方法:
构造方法 | 描述 |
ServerSocket ( int port ) | 创建一个服务端流套接字Socket,并绑定指定端口 |
方法:
方法 | 描述 |
Socket accept ( ) | |
void close ( ) | 关闭此套接字 |
1.2 Socket
Socket 是客户端Socket,或服务端中接收到客户端简历连接(accept方法)的请求后,返回服务端Socket
不管是客户端还是服务端Socket,都是双方简历连接以后,保存的对端信息,及用来与对方收发数据的
构造方法:
构造方法 | 描述 |
Socket ( String host, int port ) | 创建一个客户端流套接字,并与对应IP主机上对应端口的进程建立连接 |
方法:
方法 | 描述 |
InetAddress getInetAddress( ) | 返回套接字所连接的地址 |
InputStream getInputStream( ) | 返回此套接字的输入流 |
OutputStream getOutputStream( ) | 返回此套接字的输出流 |
二、模拟回显服务器
public class TCPEchoServer {
private ServerSocket serverSocket = null;
public TCPEchoServer (int port) throws IOException {
serverSocket = new ServerSocket(port); // 创建TCP 服务端Socket并绑定端口号
}
}
TCP是面向有连接型的,客户端要先和服务器成功建立连接才能发送数据
public class TCPEchoServer {
private ServerSocket serverSocket = null;
public TCPEchoServer (int port) throws IOException {
serverSocket = new ServerSocket(port); // 创建 TCP 服务端Socket并绑定端口号
}
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
Socket clientSocket = serverSocket.accept();
processConnection(clientSocket); // 通过这个方法处理连接
}
}
}
服务器启动后就要代码就会走到accept方法,如果没有客户端连接上来,accept就会阻塞,直到有客户端连接上来;客户端连接上来后,通过processConnection方法处理这个连接
private void processConnection (Socket clientSocket) throws IOException {
//1. 打印一个日志,告知说当前有客户端连上了
System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
//2. 从 clientsocket 中获取流对象,来进一步的进行 后续操作
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
} catch (IOException e) {
e.printStackTrace();
} finally {
clientSocket.close();
}
获取到流对象后,就要从客户端的输入流中读取请求,我们可以采用inputStream.read(byte[ ] buf)的方式读取,但是读取的结果是一个byte[ ],在处理请求时还要转化为一个字符串,而且一次连接不止一个请求,一个byte[ ]里面可能有多给请求,转化为字符串后不易将不同的请求区分;所以这里采用Scanner
Scanner scanner = new Scanner(inputStream); //使用Scanner读取客户端的输入流中的数据
while (true) { //设置循环,循环读取多个请求
//3.1 读取请求并解析
// scanner.hasNext收到了请求且有明确的分隔符则返回true
if (!scanner.hasNext()) { //若tcp断开则返回false
System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress(), clientSocket.getPort()); //souf
break;
}
String request = scanner.next(); //读取输入流中的请求
}
在客户端发送的每一个请求后面都加上一个'\n',而scanner.hasNext( )就是在扫描输入流中是否还有"标记",这个标记就是指空白符(空格、制表符、换行符等),而我们每一个请求后面都会加上换行符,所以只要输入流中还有请求,循环就会继续
//3.2 根据请求计算响应
String response = process(request);
//3.3 把响应写回给客户端, 写到客户端Socket的输出流中
outputStream.write(response.getBytes(), 0, response.getBytes().length);
//3.4 服务器打印日志
System.out.printf("[%s:%d] req=%s, resp=%s\n", clientSocket.getInetAddress(), clientSocket.getPort(), request, response);
返回给客户端的响应有很多,所以每个响应的后面都要加上换行符
private String process (String request) {
return request + "\n";
}
服务器完整代码:
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
Socket clientSocket = serverSocket.accept(); //客户端没有连接上来,这里就会进入阻塞
processConnection(clientSocket);
}
}
private void processConnection (Socket clientSocket) throws IOException {
//1. 打印一个日志,告知说当前有客户端连上了
System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
//2. 从socket 中获取流对象,来进一步的进行 后续操作
try (InputStream inputStream = clientSocket.getInputStream(); //!!!!从客户端的socket连接获取输入流和输出流
OutputStream outputStream = clientSocket.getOutputStream()) {
//使用Scanner读取客户端的输入流
Scanner scanner = new Scanner(inputStream);
//3. 也搞一个循环,针对每个连接,客户端都可能会发来多个请求,服务器也就需要返回多个响应了
while (true) {
//3.1 读取请求并解析
//scanner.hasNext收到了请求且有明确的分隔符则返回true
if (!scanner.hasNext()) { //若tcp断开则返回false
System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress(), clientSocket.getPort()); //souf
break;
}
String request = scanner.next(); //读取输入流中的请求
//3.2 根据请求计算响应
String response = process(request);
//3.3 把响应写回给客户端, 写到客户端Socket的输出流中
outputStream.write(response.getBytes(), 0, response.getBytes().length);
//3.5 服务器打印日志
System.out.printf("[%s:%d] req=%s, resp=%s\n", clientSocket.getInetAddress(), clientSocket.getPort(), request, response);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
clientSocket.close();
}
}
private String process (String request) {
return request + "\n"; //每个响应结尾加上换行符
}
public static void main(String[] args) throws IOException {
TCPEchoServer server = new TCPEchoServer(9090);
server.start();
}
}
接下来编写客户端代码:
public class TCPEchoClient {
private Socket socket = null;
public TCPEchoClient(String serverIp, int serverPort) throws IOException {
socket = new Socket(serverIp, serverPort);
}
上述构造方法本身就能和指定的服务器申请连接,然后服务器那边调用accept方法后就能成功建立连接,建立连接的过程都是操作系统内核完成的
public void start() {
System.out.println("客户端启动!");
Scanner scanner = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner scannerNetwork = new Scanner(inputStream); //读取输入流中服务器返回的响应
while (true) {
}
} catch (IOException e) {
e.printStackTrace();
}
}
接下来在循环中,客户端可以不断发送请求并返回响应
//1. 从控制台读取数据
System.out.print("请输入要发送的数据: ");
String request = scanner.next(); //这个scanner只能读取控制台的数据,和输入流无关
之前约定每个请求后面都要加上换行符,这里request后面没有换行符,所以要手动加上后再写到输入流中,写入输入流就相当于将请求发送给了服务器
//2. 把请求发送给服务器
request += "\n";
outputStream.write(request.getBytes());
【疑问】为什么说这样就是将请求发送给了服务器?
【解答】在客户端程序中通过Socket的构造方法申请与服务器端建立连接,然后在服务器端程序中,通过accept方法接收了该连接,此时客户端和服务器端的两个Socket对象:socket和clientSocket,它们就是彼此关联的,通过他们的流对象就可以互相进行读写操作,比如: clientSocket的输出流对象outputStream通过write方法写入响应,socket可以通过Scanner(inputStream)读取响应,反过来也可以
//3. 从服务器读取到响应
if (!scannerNetwork.hasNext()) {
break;
}
String response = scannerNetwork.next(); //scannerNetwork是扫描输入流的,和控制台无关
//4. 把响应显示到控制台上
System.out.println(response);
上述代码,服务器将相应写回到输出流中,客户端通过Scanner读取响应
完整客户端代码如下:
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("客户端启动!");
Scanner scanner = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner scannerNetwork = new Scanner(inputStream);
while (true) {
//1. 从控制台读取数据
System.out.print("请输入要发送的数据: ");
String request = scanner.next();
//2. 把请求发送给服务器
request += "\n";
outputStream.write(request.getBytes());
//3. 从服务器读取到响应
if (!scannerNetwork.hasNext()) {
break;
}
String response = scannerNetwork.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();
}
}
接下来同时启动服务器程序和客户端程序:
通过控制台输入请求:
上述服务器程序中还有个问题:因为只有一个processConnection方法,所以它一次只能处理一个客户端连接,当有第二个客户端连接时,必须等到第一个客户端断开连接才能建立第二个连接,这是不合理的,所以可以引入多线程,这样可以处理多个客户端连接
Thread t = new Thread (() -> {
try {
processConnection(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
});
t.start();
🙉本篇文章到此结束