网络编程套接字2
之前我们已经介绍了UDP套接字流程,接下来我们介绍TCP流套接字编程,TCP的一个核心特点,面向字节流,读写数据的基本单位就是字节。
1.API介绍
1.1ServerSocket:是创建TCP服务器Socket的API(专门给服务器用);
方法:
(服务器启动需要先绑定端口号)
TCP是“有连接”,这里的accept是联通连接的关键操作。
2.Socket
Socket是客户端Socket,或服务端中接收到客⼾端建⽴连接(accept⽅法)的请求后,返回的服 务端Socket(服务器和客户端都可以用)。
方法:
这两个参数是服务器的IP和服务器的端口。
代码实例:
TcpEchoServer
public class TcpEchoServer {
private ServerSocket serverSocket = null;
// 这里和 UDP 服务器类似, 也是在构造对象的时候, 绑定端口号.
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("启动服务器");
// 这种情况一般不会使用 fixedThreadPool, 意味着同时处理的客户端连接数目就固定了.
ExecutorService executorService = Executors.newCachedThreadPool();
while (true) {
// tcp 来说, 需要先处理客户端发来的连接.
// 通过读写 clientSocket, 和客户端进行通信.
// 如果没有客户端发起连接, 此时 accept 就会阻塞.
// 主线程负责进行 accept, 每次 accept 到一个客户端, 就创建一个线程, 由新线程负责处理客户端的请求.
Socket clientSocket = serverSocket.accept();
// 使用多线程的方式来调整
// Thread t = new Thread(() -> {
// processConnection(clientSocket);
// });
// t.start();
// 使用线程池来调整
executorService.submit(() -> {
processConnection(clientSocket);
});
}
}
// 处理一个客户端的连接.
// 可能要涉及到多个客户端的请求和响应.
private void processConnection(Socket clientSocket) {
System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
// 针对 InputStream 套了一层
Scanner scanner = new Scanner(inputStream);
// 针对 OutputStream 套了一层
PrintWriter writer = new PrintWriter(outputStream);
// 分成三个步骤
while (true) {
// 1. 读取请求并解析. 可以直接 read, 也可以借助 Scanner 来辅助完成.
if (!scanner.hasNext()) {
// 连接断开了
System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
break;
}
String request = scanner.next();
// 2. 根据请求计算响应
String response = process(request);
// 3. 返回响应到客户端
// outputStream.write(response.getBytes());
writer.println(response);
writer.flush();
// 打印日志
System.out.printf("[%s:%d] req: %s, resp: %s\n", clientSocket.getInetAddress(), clientSocket.getPort(),
request, response);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
clientSocket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
服务器引⼊多线程, 如果只是单个线程,⽆法同时响应多个客⼾端,此处给每个客⼾端都分配⼀个线程.
// 启动服务器
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
Socket clientSocket = serverSocket.accept();
Thread t = new Thread(() -> {
processConnection(clientSocket);
});
t.start();
}
}
服务器引⼊线程池 为了避免频繁创建销毁线程,也可以引⼊线程池.
// 启动服务器
public void start() throws IOException {
System.out.println("服务器启动!");
ExecutorService service = Executors.newCachedThreadPool();
while (true) {
Socket clientSocket = serverSocket.accept();
// 使⽤线程池, 来解决上述问题
service.submit(new Runnable() {
@Override
public void run() {
processConnection(clientSocket);
}
});
}
}
TCPEchoClient
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String serverIp, int serverPort) throws IOException {
// 直接把字符串的 IP 地址, 设置进来.
// 127.0.0.1 这种字符串
socket = new Socket(serverIp, serverPort);
}
public void start() {
Scanner scanner = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
// 为了使用方便, 套壳操作
Scanner scannerNet = new Scanner(inputStream);
PrintWriter writer = new PrintWriter(outputStream);
// 从控制台读取请求, 发送给服务器.
while (true) {
// 1. 从控制台读取用户输入
String request = scanner.next();
// 2. 发送给服务器
writer.println(request);
// 加上刷新缓冲区操作, 才是真正发送数据
writer.flush();
// 3. 读取服务器返回的响应.
String response = scannerNet.next();
// 4. 打印到控制台
System.out.println(response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
client.start();
}
}
1. 创建socket对象就会在底层和对端建立TCP连接,及记录了对端的信息(服务器的IP和端口),不需要自己在创建变量保存了,直接TCP内部就保存了。
2.
这两个socket对象不是同一个对象(它们在不同的进程中,甚至在不同的主机上)。
3.行为是自动加上\n
这个操作只是把数据放到“发送缓冲区”中,还没有真正写入到网卡中;如果这里使用print,则数据发过去,服务器收到了,但没有真正处理。
暗含一个约定,一个请求/响应,使用\n作为结束标记,对端读的时候,也是读到\n就结束(认为读到一个完整的请求)。
4.flush方法来“冲刷缓冲区”,才算真正的发送数据。
5.
判断收到的数据中是否包含“空白符”,遇到空白符(比如换行、回车、空格、制表符、翻页符...),认为一个“完整的next”,遇到之前都会堵塞。
6.
在这里clientSocket的生命周期是伴随一次连接的,每个客户端连接,都会创建一个新的;每个客户端断开连接,这个对象也就可以不要了,这时就需要加一个close(),防止文件的泄露。
TCP发送数据时,需要先建⽴连接,什么时候关闭连接就决定是短连接还是⻓连接:
- 短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能⼀次收发 数据。
- ⻓连接:不关闭连接,⼀直保持连接状态,双⽅不停的收发数据,即是⻓连接。也就是说,⻓连接可 以多次收发数据。
对⽐以上⻓短连接,两者区别如下:
- 建⽴连接、关闭连接的耗时:短连接每次请求、响应都需要建⽴连接,关闭连接;⽽⻓连接只需要 第⼀次建⽴连接,之后的请求、响应都可以直接传输。相对来说建⽴连接,关闭连接也是要耗时 的,⻓连接效率更⾼。
- 主动发送请求不同:短连接⼀般是客⼾端主动向服务端发送请求;⽽⻓连接可以是客⼾端主动发送 请求,也可以是服务端主动发。
- 两者的使⽤场景有不同:短连接适⽤于客⼾端请求频率不⾼的场景,如浏览⽹⻚等。⻓连接适⽤于 客⼾端与服务端通信频繁的场景,如聊天室,实时游戏等。
原文地址:https://blog.csdn.net/2301_80501383/article/details/143765944
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!