目录
TCP特点
1.有连接:通信双方都建立好连接,才能进行通信;那无连接是什么?便是通信双方在不建立连接的情况下也可以通信;
2.可靠传输:A给B传输信息,A可以知道B是否接收到(复杂的网络环境不能保证百分百B能接收到数据)数据;那可靠传输又是什么?便可想而知了;
3.面向字节流:以字节为基本单位;
4.全双工:一个通道,双向通信(同时上传和下载),为何一个通道可以双向通信?这一个通道里不止一个网线,例如有8根,那么就会分成两组:4进4出;(全双工的对立面是——单双杠:一个通道,单向通信);
基于TCP建立服务端
Java 如下:
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;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
//服务器
public class TcpEchoServer {
private ServerSocket listenSocket = null;
public TcpEchoServer(int port) throws IOException {
listenSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
//通过线程池来服务多个客户端(循环创建多线程,频繁创建销毁线程,高并发的情况下,负担还是很重的,所以这里使用线程池)
ExecutorService service = Executors.newCachedThreadPool();
while(true) {
//通过调用accept来接受请求,若没有客户端来建立连接就会阻塞等待
Socket clientSocket = listenSocket.accept();
//通过线程池来解决频繁创建销毁线程的问题
service.submit(new Runnable() {
@Override
public void run() {
try {
processConnection(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
public void processConnection(Socket clientSocket) throws IOException {
System.out.printf("[%s:%s] 客户端上线!\n", clientSocket.getInetAddress().toString(),
clientSocket.getPort());
//处理客户端请求
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
while(true) {
//1.读取请求并解析
Scanner scanner = new Scanner(inputStream);
if(!scanner.hasNext()) {
//客户端断开连接的时候,hasNext()就会返回false,所以,客户端一下线就结束该线程
System.out.printf("[%s:%s] 客户端下线!\n", clientSocket.getInetAddress().toString(),
clientSocket.getPort());
break;
}
String request = scanner.next();
//2.根据请求计算响应
String response = process(request);
//3.将响应写回到服务器
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response);
//刷新缓冲数据区,确保确实将数据写入
printWriter.flush();
//打印日志
System.out.printf("[%s:%s] req: %s, resp: %s\n", clientSocket.getInetAddress().toString(),
clientSocket.getPort(), request, response);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//clientSocket在循环中,每来一个客户端就会为他分配一个;
//对象会反复被new出实例,每创建一个,都要消耗一个文件描述符;
//因此就要把不需要的clientSocket释放掉
clientSocket.close();
}
}
//这是一个回显服务器
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
Kotlin 如下:
import java.io.IOException
import java.io.PrintWriter
import java.net.ServerSocket
import java.net.Socket
import java.util.Scanner
import java.util.concurrent.Executors
class TCPServer(
port: Int
) {
private val serverSocket = ServerSocket(port)
fun start() {
println("TCP 服务器启动!")
while(true) {
val client = serverSocket.accept()
val pool = Executors.newCachedThreadPool()
pool.submit {
clientHandler(client)
}
}
}
private fun clientHandler(client: Socket) {
println("客户端上线: address: ${client.inetAddress}, port: ${client.port}")
try {
client.getInputStream().use { inputStream ->
client.getOutputStream().use { outputStream ->
val scanner = Scanner(inputStream)
val writer = PrintWriter(outputStream)
while(true) {
//1.读取请求并解析
if(!scanner.hasNext()) {
println("客户端下线: address: ${client.inetAddress}, port: ${client.port}")
}
val request = scanner.next()
//2.根据请求计算响应
val response = process(request)
//3.返回响应
writer.println(response)
writer.flush()
//4.记录日志
println("[${client.inetAddress}:${client.port}] req: $request")
}
}
}
} catch (e: IOException) {
e.printStackTrace()
} finally {
client.close()
}
}
/**
* 回显服务
*/
private fun process(request: String): String {
return request
}
}
fun main() {
val server = TCPServer(9090)
server.start()
}
问题1:为什么finally那里要进行clientSocket.close() ?listenSocket不用释放吗?
clientSocket在循环中,每来一个客户端就会为他分配一个,对象会反复被new出实例,每创建一个,都要消耗一个文件描述符,因此就要把不需要的clientSocket释放掉;
listenSocket在TCP服务器中只有唯一一个对象,并且随着进程的退出自定释放,不会把文件描述符表占满;
问题2:为什么要用线程池?
有两个概念有必要了解一下:
长连接:一个连接处理多个请求 (TCP建立连接后,要处理客户端的多次请求);
短链接:一个连接处理一个请求(TCP每个连接只处理一个客户端请求);
想要一个连接会处理 N 个请求和响应,就需要使用多线程;但是单单用循环来创建多线程可行吗?可行是可行,但是一旦需要频繁创建销毁线程,高并发的情况下,负担还是很重的,所以通过线程池来服务多个客户端;
问题3:printWriter.println后面的println为什么要加ln?可以不加吗?
这里隐式约定了应用层协议的格式,一个请求是以\n结尾的;
基于TCP建立客户端
Java 如下:
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 {
private Socket socket = null;
public TcpEchoClient() throws IOException {
//new 这个对象的时候就需要和服务器建立连接,就要知道服务器在哪
socket = new Socket("127.0.0.1", 9090);
}
public void start() {
//长连接,一个连接会处理N个请求和响应
Scanner scanner = new Scanner(System.in);
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner scanSocket = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
while(true) {
//1.从控制台读入请求
System.out.print("->");
String request = scanner.next();
//2.将请求发给客户端
printWriter.println(request);
//刷新缓存区,确保信息发送
printWriter.flush();
//3.从服务器读取响应
String response = scanSocket.next();
//4.打印响应
System.out.println(response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient();
client.start();
}
}
Kotlin 如下:
import java.io.PrintWriter
import java.net.Socket
import java.util.Scanner
class TCPClient(
ip: String,
port: Int,
) {
private val client = Socket(ip, port)
fun start() {
client.getInputStream().use { inputStream ->
client.getOutputStream().use { outputStream ->
val clientScan = Scanner(System.`in`) //用户输入
val serverScan = Scanner(inputStream) //服务器输入
val write = PrintWriter(outputStream)
while(true) {
//1.控制台输入请求
print("client req -> ")
val request = clientScan.next()
//2.请求发送到服务器
write.println(request)
write.flush()
//3.读取服务器响应
val response = serverScan.next()
//4.处理响应
println("server resp -> $response")
}
}
}
}
}
fun main() {
val client = TCPClient("127.0.0.1", 9090)
client.start()
}
执行效果如下: