TCP套接字编程
- 1.ServerSocket API
- 1.1ServerSocket 的构造方法
- 1.2ServerSocket 方法:
- 2.Socket API
- 2.1Socket构造方法
- 2.2Socket方法
- 3.TCP回显服务器
- 4.TCP中的长短连接
- 5.C10M问题
TCP提供的API主要有两个类:一个是专门给服务器用的SeverSocket对象,一个是既可以给服务器端用也可以给客户端用的Socket对象。
1.ServerSocket API
1.1ServerSocket 的构造方法
1.2ServerSocket 方法:
2.Socket API
Socket是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端.不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据
的。
2.1Socket构造方法
2.2Socket方法
3.TCP回显服务器
服务器端
package network;
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;
public class TCPEchoSever {
private ServerSocket serverSocket = null;
public TCPEchoSever(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
//socket对象用于与具体的客户端进行数据交流
//severSocket只用于客户端与服务器端的连接
System.out.println("服务器启动!");
while(true){
Socket socket = serverSocket.accept();
processConection(socket);
}
}
//使用这个方法来处理一个连接
//一个连接对应一个客户端,但是这里可能涉及到多次交互
public void processConection (Socket socket) throws IOException {
System.out.printf("[%s,%d]客户端上线!\n", socket.getInetAddress().toString(),socket.getPort());
//基于上述socket对象与客户端进行通信
try (InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()) {
while(true){
//构建从缓冲区读取数据对象scanner
Scanner scanner = new Scanner(inputStream);
//判断缓冲区里面有么有数据了,如果么有了就代表客户端下线
if(!scanner.hasNext()){
System.out.printf("[%s,%d]客户端下线!\n",socket.getInetAddress().toString(),socket.getPort());
break;
}
//注意:此时使用的next是一直读取到换行符、空格、其他空白符结束,但是最终返回结果里不包含空白符
//1.读取请求
String request = scanner.next();
//2.根据请求构建响应
String respose = process(request);
//3.将响应写入输出的缓冲区
//构建一个打印流
PrintWriter printWriter = new PrintWriter(outputStream);
//将响应写入打印流
printWriter.println(respose);
printWriter.flush();
System.out.printf("[%s,%d],req:%s,resp:%s\n",socket.getInetAddress().toString(),socket.getPort(),request,respose);
}
}
finally {
socket.close();//socket本身也是一个文件,也要占用文件描述表里面的一个位置,用完也是需要关闭的。前面的实例中都没有这个要求,那是因为UDP的socket声明周期是整个
//通信过程,所以没有关闭,而在tcp中,socket只用于与客户端的数据通信,不涉及连接,连接交给了severSocket
}
}
public String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
TCPEchoSever tcpEchosever = new TCPEchoSever(9090);
tcpEchosever.start();
}
}
客户端
package network;
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 {
//一定主要客户端的socket和服务器的socket不是一个对象,而是两个,一个用于服务器一个用于客户端,类似于打电话的两个电话,但是两个socket是有关联的
//当客户端的socke创建的时候(被new的时候)服务器端的socket就会通过serverSocket.accept()的返回值创建出一个相关联的socket
private Socket socket = null;
public TCPEchoClient( String severIp, int port) throws IOException {
//new这个对象的同时就会进行TCP的连接操作(四次握手)
this.socket = new Socket(severIp,port);
}
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.println("请输入信息:");
String request = scanner.next();
if (request.equals("exit")) {
System.out.println("请求结束!");
return;
}
//2.把读到的内容构造成请求(字符型的打印流),发送给服务器
PrintWriter printWriter = new PrintWriter(outputStream);
//注意这不是打印,而是向printWriter里面写入的一个带换行符一串字符request
printWriter.println(request);
//在冲刷一下保证全部写入
printWriter.flush();
//3.读取服务器的响应
Scanner responseScanner = new Scanner(inputStream);
String response = responseScanner.next();
//4.打印响应
System.out.println(response);
}
}catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TCPEchoClient tcpEchoClient = new TCPEchoClient("127.0.0.1",9090);
tcpEchoClient.start();;
}
}
结果
severSocket.accept();
效果是接收连接,前提是。得有客户端来建立连接,客户端在构造Socket对象的时候,就会指定服务器的ip和端口号,但是如果么有客户端来连接,那么accept()就会阻塞。severSocket.accept();
返回值是一个Socket对象。- outputStream相当于对应着一个文件描述符(sockte文件的文件描述符),我们通过outputStream就可以往这个文件描述符里面写数据了。但是因为outputStream本身的方法不方便写字符串,所以我们就将这个流转换了一下,用一个PrintWriter对象来表示(对于的文件描述符还是同一个);这样我们可以使用PrintWriter的prinln方法往文件里面写入字符串了。PrintWriter写 和OutputStream写是往同一个地方写,只不过写的更方便了。
printWriter.println(request);
这一句中将println换成print可不可以呢?答案是不可以的。TCP是面向字节流的协议(字节流的特征就在于读写以字节为单位,一次读多少个字节,都行),那么此时接收方如何知道,这一次一共要读多少字节呢?,这里我们就采用是println在输入的字符串后面加上\n来作为当前代码请求或者响应的分割。此处还有一个疑问在于输入的时候不也按下了换行符(按下了回城键)吗?String request = scanner.next();
但是很遗憾的是scanner.next()这个方法是读取到换行符结束(但是读取的字符串里不包含换行符)。- 此外再提一点,这个情况在C里面会导致下一次scanner的时候,会直接读取到\n而直接结束读取,但是这种情况实际在java里不会出现,JVM中,这个\n也会被读取走,但是不作为返回值。
当前的代码还有一个大问题,那就是当前的服务器是指只能服务一个客户端程序的,如果启动第二个实例,实际第二个实例是没有办法正常工作的
第一个客户端正常工作
第二个客户端没有收到服务器的响应
服务器端也是么有响应第二个客户端的此时如果我们将第一个客户端关闭,那么第二个客户端就会工作就会正常得到响应;
第一个客户端
第二个客户端
服务器端
那么是什么原因呢?
是这样的,我们的服务器代码里实际是两层循环
while(true){
Socket socket = serverSocket.accept();
processConection(socket);
}
public void processConection (Socket socket) throws IOException {
System.out.printf("[%s,%d]客户端上线!\n", socket.getInetAddress().toString(),socket.getPort());
//基于上述socket对象与客户端进行通信
try (InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()) {
while(true){
//构建从缓冲区读取数据对象scanner
Scanner scanner = new Scanner(inputStream);
//判断缓冲区里面有么有数据了,如果么有了就代表客户端下线
if(!scanner.hasNext()){
System.out.printf("[%s,%d]客户端下线!\n",socket.getInetAddress().toString(),socket.getPort());
break;
}
//注意:此时使用的next是一直读取到换行符、空格、其他空白符结束,但是最终返回结果里不包含空白符
//1.读取请求
String request = scanner.next();
//2.根据请求构建响应
String respose = process(request);
//3.将响应写入输出的缓冲区
//构建一个打印流
PrintWriter printWriter = new PrintWriter(outputStream);
//将响应写入打印流
printWriter.println(respose);
printWriter.flush();
System.out.printf("[%s,%d],req:%s,resp:%s\n",socket.getInetAddress().toString(),socket.getPort(),request,respose);
}
}catch (IOException e){
e.printStackTrace();
}
finally {
socket.close();//socket本身也是一个文件,也要占用文件描述表里面的一个位置,用完也是需要关闭的。前面的实例中都没有这个要求,那是因为UDP的socket声明周期是整个
//通信过程,所以没有关闭,而在tcp中,socket只用于与客户端的数据通信,不涉及连接,连接交给了severSocket
}
}
所以就如果说第一个客户端与服务端连接了,那么就执行了Socket socket = serverSocket.accept(); processConection(socket);
这两句,而且如果第一个客户端只要不输入exit,那么代码会一直卡在processConection里面while循环里,此时第二个客户端来,服务器端的代码仍在while循环里,没有办法重新执行accept,所以也就没有办法处理第二个客户端的请求。
那么我们如何来解决呢?可以采用线程的思想,我们可以用主线程专门负责accept,每次accept都去创建一个新的线程去处理新的请求。每个线程是一个独立的执行流,彼此之间的并发关系。
所以我们只需要在每次accept之后去创建一个线程,让这个线程去执行procesConnaction就行
while(true){
Thread t = new Thread(()->{
try {
Socket socket = serverSocket.accept();
processConection(socket);
} catch (IOException e) {
e.printStackTrace();
}
});
t.start();
}
更加好一点的方式是使用线程池的方式
ExecutorService threadPool = Executors.newCachedThreadPool();//构建一个动态大小的线程池
while(true){
Socket socket = serverSocket.accept();
threadPool.submit(()->{
try {
processConection(socket);
} catch (IOException e) {
e.printStackTrace();
}
});
}
结果
注意:
- TCP连接有短连接和长连接的区别,短连接是请求后客户端与服务器端只会有一次数据交换,之后就释放连接了。但是长连接是建立连接后客户端和服务器端会进行多次的数据传输后才会释放连接。而在代码里体现的就是是否在processConenaction里是否会有循环。
4.TCP中的长短连接
TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接。短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能一次收发数 据。长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可以 多次收发数据。
对比以上长短连接,两者区别如下:
建立连接、关闭连接的耗时:短连接每次请求、响应都需要建立连接,关闭连接;而长连接只需要 第一次建立连接,之后的请求、响应都可以直接传输。相对来说建立连接,关闭连接也是要耗时 的,长连接效率更高。
主动发送请求不同:短连接一般是客户端主动向服务端发送请求;而长连接可以是客户端主动发送 请求,也可以是服务端主动发。
两者的使用场景有不同:短连接适用于客户端请求频率不高的场景,如浏览网页等。长连接适用于 客户端与服务端通信频繁的场景,如聊天室,实时游戏等。扩展了解。
基于BIO(同步阻塞IO)的长连接会一直占用系统资源。对于并发要求很高的服务端系统来说,这样的 消耗是不能承受的。
由于每个连接都需要不停的阻塞等待接收数据,所以每个连接都会在一个线程中运行。
一次阻塞等待对应着一次请求、响应,不停处理也就是长连接的特性:一直不关闭连接,不停的 处理请求。实际应用时,服务端一般是基于NIO(即同步非阻塞IO)来实现长连接,性能可以极大的提升。
5.C10M问题
在实际开发中,可能服务器端只有几个这样的数量级,但是客户端的数量是非常庞大的,如果有几万个客户端,我们就要创建几万个线程这样可能机器可能还能承受,但是如果十万级别的线程数,我们除了去增加成本增加服务器数量,还可以用什么办法呢?所谓C10M问题。C10K问题就是如何解决单机处理1w客户端的问题,所以C10M问题是说单机如何处理比一万多很多的线程开销(并不是一定说处理1kw个线程)
要知道,这个主要解决的无非是线程开销问题,那么有没有什么办法可以做到一个线程去处理多个客户端连接呢?——IO多路复用。
那么为什么一个线程可以处理多个请求呢?因为多个请求本身也是交替上处理机 的,并不是一个请求一直占用着处理机。所以我们可以 这样处理,给这个线程安排个集合,这个集合就放一堆连接,这个线程就负责监听这个集合,哪个连接有数据来了。线程就来处理哪个连接。在操作系统里提供里一些原生API,在java里封装了很多NIO。
在这里我们就不过多介绍了。
那么至此,我们TCP的套接字编程就介绍到这里。