文章目录
说明:这部分说实话有点懵,理解上有点吃力,这里暂时先放到这,有新的认识再进行回来修改。
一、ServerSocket API
它是创建TCP服务端Socket的api
构造方法
方法签名 | 说明 |
---|---|
ServerSocket(int port) | 创建一个服务端流套接字Socket,并绑定到指定端口 |
给服务器绑定端口。
常用方法
方法签名 | 说明 |
---|---|
Socket accept() | 开始监听创建时绑定的端口,有客户端连接后,返回一个服务端Socket 对象,并基于该Socket建立与客户端的连接,否则阻塞等待 |
void close() | 关闭此套接字 |
二、Socket API
即会给客户端和服务器使用。
构造方法
方法签名 | 说明 |
---|---|
Socket(String host, int port) | 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的 进程建立连接 |
常用方法
方法签名 | 说明 |
---|---|
InetAddress getInetAddress() | 返回套接字所连接的地址 |
InputStream getInputStream() | 返回此套接字的输入流 |
OutputStream getOutputStream() | 返回此套接字的输出流 |
TCP中,socket对象,对于服务器而言,是靠accpet返回的;对于客户端而言,是靠代码内部构造的。
注意事项
-
TCP 中的ClientSocket的socket对象需要释放,而前边Server对象和UDP的都没释放,为什么这里需要呢?原因有二
1.这里的socket声明周期比较短,UDP里边的和TCP服务器里的是要跟随整个程序的。
2.这里的socket对象可能比较多,可能会把文件描述符表占满。
outputStream相当于一个文件描述符(一个socket文件),通过这个对象就可以往这个文件描述符中写数据。
OutStream自身方法不方便写字符串,把流进行转换一下,用一个PW对象来表示(对应的文件还是通过一个)。
不过使用PW(打印流)写,往一个地方写,只不过的写的更方便了。
println是写,往控制台上,往网卡上……
三、TCP中的长短连接
客户端socket对象构造,会触发tcp建立连接
短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能一次收发数据。
长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可以多次收发数据。
两者区别如下:
建立连接、关闭连接的耗时:短连接每次请求、响应都需要建立连接,关闭连接;而长连接只需要第一次建立连接,之后的请求、响应都可以直接传输。相对来说建立连接,关闭连接也是要耗时的,长连接效率更高。主动发送请求不同:短连接一般是客户端主动向服务端发送请求;而长连接可以是客户端主动发送请求,也可以是服务端主动发。
两者的使用场景有不同:短连接适用于客户端请求频率不高的场景,如浏览网页等。长连接适用于客户端与服务端通信频繁的场景,如聊天室,实时游戏等。
E1:一发一收(短连接)
服务器端代码、客户端代码
public class Code04_TCPEchoServer {
private ServerSocket serverSocket=null;//这种可以返回Socket类型的对象
public Code04_TCPEchoServer(int port) throws IOException {
serverSocket=new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("启动服务器成功!");
while(true){
Socket clientSocket=serverSocket.accept();
//效果是接收连接
//前提是客户端来建立连接:若有则连接,若无,则阻塞等待
//建立连接
processConnection(clientSocket);
}
}
//使用这个方法来处理一个连接
//这个连接对应到一个客户端,但是这里可能会设计到多次交互
private void processConnection(Socket clientSocket) {
System.out.printf("[%s:%d] 客户端上线!",clientSocket.getInetAddress().toString(),clientSocket.getPort());
//基于上述socket进行通信
try(InputStream inputStream=clientSocket.getInputStream();
//由于要处理多个请求,所以也是使用循环来进行
OutputStream outputStream=clientSocket.getOutputStream()){
while(true){
//1.读取请求
Scanner scanner=new Scanner(inputStream);
if(!scanner.hasNext()){
// System.out.println("当前连接已关闭!");
System.out.printf("[%s:%d] 客户端下线!",clientSocket.getInetAddress().toString(),clientSocket.getPort());
break;
}
System.out.println();
String request=scanner.next();//next读到换行符/其他空白符结束,但是不包含
//2.根据请求构造响应
String response=process(request);
//3.返回响应结果
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response);
// 此处加上 flush 保证数据确实发送出去了.
printWriter.flush();
//4.打印中间结果
System.out.printf(response);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try{
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
Code04_TCPEchoServer tcpEchoServer=new Code04_TCPEchoServer(1200);
tcpEchoServer.start();
}
}
public class Code05_TCPEchoClient {
private Socket socket = null;//这是用来接收服务器的socket对象
public Code05_TCPEchoClient(String serverIp, int serverPort) throws IOException {
// Socket 构造方法, 能够识别 点分十进制格式的 IP 地址. 比 DatagramPacket 更方便.
// new 这个对象的同时, 就会进行 TCP 连接操作.
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()) {
while (true) {
// 1. 先从键盘上读取用户输入的内容
System.out.print("> ");
String request = scanner.next();
if (request.equals("exit")) {
System.out.println("bye");
break;
}
// 2. 把读到的内容构造成请求, 发送给服务器.
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
// 此处加上 flush 保证数据确实发送出去了.
printWriter.flush();
// 3. 读取服务器的响应
Scanner respScanner = new Scanner(inputStream);
String response = respScanner.next();
// 4. 把响应内容显示到界面上
System.out.println(response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
Code05_TCPEchoClient client = new Code05_TCPEchoClient("127.0.0.1", 1200);
client.start();
}
}
这里不用println写是不行的,原因如下:
TCP协议是面向字节流的协议。接收方一次读多少个字节需要我们在数据传输中进行明确约定。
这里的next和println是相互制约的。next在等请求中的结束符。
enter是换行符,但是这里按下enter是把next里内容送上去,并没有把换行符读到。
E2:请求响应(短连接)
略
与UDP类似,这里是在服务器处,加上了相关的业务逻辑。
E3:多线程下的TCP回响服务器
public class Code04_TCPEchoServer {
private ServerSocket serverSocket=null;//这种可以返回Socket类型的对象
public Code04_TCPEchoServer(int port) throws IOException {
serverSocket=new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("启动服务器成功!");
ExecutorService threadPool= Executors.newCachedThreadPool();
while(true){
Socket clientSocket=serverSocket.accept();
//效果是接收连接
//前提是客户端来建立连接:若有则连接,若无,则阻塞等待
//建立连接
threadPool.submit(()->{
processConnection(clientSocket);
});
}
}
//使用这个方法来处理一个连接
//这个连接对应到一个客户端,但是这里可能会设计到多次交互
private void processConnection(Socket clientSocket) {
System.out.printf("[%s:%d] 客户端上线!",clientSocket.getInetAddress().toString(),clientSocket.getPort());
//基于上述socket进行通信
try(InputStream inputStream=clientSocket.getInputStream();
//由于要处理多个请求,所以也是使用循环来进行
OutputStream outputStream=clientSocket.getOutputStream()){
while(true){
//1.读取请求
Scanner scanner=new Scanner(inputStream);
if(!scanner.hasNext()){
// System.out.println("当前连接已关闭!");
System.out.printf("[%s:%d] 客户端下线!",clientSocket.getInetAddress().toString(),clientSocket.getPort());
break;
}
System.out.println();
String request=scanner.next();//next读到换行符/其他空白符结束,但是不包含
//2.根据请求构造响应
String response=process(request);
//3.返回响应结果
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response);
// 此处加上 flush 保证数据确实发送出去了.
printWriter.flush();
//4.打印中间结果
System.out.printf(response);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try{
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
Code04_TCPEchoServer tcpEchoServer=new Code04_TCPEchoServer(1200);
tcpEchoServer.start();
}
}
1.修改允许多个客户端其中之后服务器只显示一个客户端上线
2.客户端1发送消息可以,客户端2发送消息没有响应【占线】
一旦客户端1一下线,客户端2立即上线。
为什么?这里需要我们结合服务器的启动代码分析
这里是多线程的问题。(其实还是有点不太懂,但具体是哪里说不上来)
当有客户端连上服务器后,代码就执行到了processConnection这个方法里的while循环,这时另一个客户端再次尝试发送请求,由于此时调到这里循环不结束,processConnection方法就结束不了,进一步也就无法再次accept了
解决办法:使用多线程。
主线程。专门负责accpet,每次收到一个连接,创建新线程,由这个新线程负责这个新的客户端
这里因为有可能频繁的申请释放线程,所以这里我们采用的是线程池。
一般的版本:
Thread t=new Thread(()->{ processConnection(clientSocket); }); t.start();
说明:虽然线程池的加入会一定程度上解决多个客户端的需要同时启动的效率问题。但是线程的创建与销毁始终还是比较耗时间的。一旦客户端的数量激增接近阈值,还是存在的问题的。
对此,操作系统提供了io多路复用的机制缓解。
【基于BIO(同步阻塞IO)的长连接会一直占用系统资源。对于并发要求很高的服务端系统来说,这样的消耗是不能承受的。实际应用时,服务端一般是基于NIO(即同步非阻塞IO)来实现长连接,性能可以极大的提升。】