一.socket
socket可以认为是操作系统中广义的文件下的一种文件类型,这样的文件类似于网卡这种硬件设备的抽象表现形式。
通过代码直接操作网卡,不好操作(网卡有很多不同的型号,之间提供的API都会有差别)操作系统就把网卡概念封装成socket,应用程序员不必关注硬件的差异和细节,统一操作socket对象,就能间接的操作网卡。
1.UDP的socket的API
DatagramSocket:
提供的方法:
DatagramPacket
代表一个UDP数据报,传递UDP数据报的基本单位
提供的方法:
回显服务器(echo server)
客户端发什么,服务器就返回什么,不存在任何业务逻辑的处理。这里只是对socket的API进行演示。
服务器:
public class UdpEchoServer {
private DatagramSocket socket = null;
public UdpEchoServer(int port) throws SocketException {
//创建socket的对象时,需要指定端口号作为构造方法的参数,port就是端口号,有了端口号才会和后续进程进行关联。
socket = new DatagramSocket(port);
//调用构造方法的过程中,JVM就会调用系统的socket API完成端口号和进程之间的关联动作
//一个端口号只能绑定一个进程,一个进程可以绑定多个端口号(创建多个socket)
}
//调用start的方法启动服务器
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
//1.读取客户端的请求并解析
//receive是从网卡上读取数据,当网卡没有数据时,receive会阻塞等待,有数据时,receive直接返回数据
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(requestPacket);
//上述收到的数据是二进制byte[]的形式体现的,后续代码需要打印之类的处理操作,需要转换成字符串
String request = new String(requestPacket.getData(),0, requestPacket.getLength());
//2.根据请求计算响应 (此处是回显服务器,响应就是请求)
String response = process(request);
//3.把响应返回客户端
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
requestPacket.getSocketAddress());
//UDP是无连接,需要通信双方保存对方的信息(IP + 端口号),DatagramSocket这个对象中,不支持对方(客户端)的IP和端口号
//进行send的时候就需要在send的数据包里,把要发给谁这样的信息,写进去,才能够正确的将数据传回
//requestPacket.getSocketAdrress就是包含客户端的IP和端口号
socket.send(responsePacket);
//4.打印日志
System.out.printf("[%s:%d] request = %s, response = %s\n",requestPacket.getAddress(),requestPacket.getPort()
,request,response);
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer server = new UdpEchoServer(8080);
server.start();
}
}
客户端:
public class UdpEchoClient {
DatagramSocket socket = null;
private String serverIP;
private int serverPort;
UdpEchoClient(String serverIP,int serverPort) throws SocketException {
//客户端最好不要指定端口号,客户端的系统会自动分配一个端口,是因为客户端指定端口号会存在这个端口号可能被客户端的其他进程占据了
//就可能会出现BUG,毕竟程序员无法知道用户的电脑使用了哪些端口
//服务器指定端口号,是为了客户端能够找到服务器
socket = new DatagramSocket();
this.serverIP = serverIP;
this.serverPort = serverPort;
}
public void start() throws IOException {
System.out.println("启动客户端");
Scanner scanner = new Scanner(System.in);
while (true) {
//1.从控制台读取用户的输入
System.out.print("-> ");
String request = scanner.next();
//2.构造出一个UDP请求,发送给服务器
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length
, InetAddress.getByName(serverIP),serverPort);//此处是客户端呢给服务器发送数据
//UDP的数据报中就需要带有目的的IP和端口,接受数据的时候,构造的UDP数据报是一个空的数据报.
socket.send(requestPacket);
//3.从服务器读取响应
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
//4.把响应打到控制台上
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.0.1",8080);
client.start();
}
}
客户端IP:127.0.0.1 随机分配的客户端端口号:62538,后续的信息则是服务器处理客户端发送的请求后的日志
2.TCP的socket的API
ServerSocket:服务器专用的socket对象
Socket: 即会给客户端使用,也会给服务器使用
Socket和ServerSocket的区别
就类似于买房子,销售楼外面会有一些销售揽客,当被揽客的销售员揽进了销售楼中,揽客的销售员就会想销售楼里面的顾问销售进行对客户的一对一分配后,揽客的销售便又去揽客了。
上述的揽客销售类似于ServerSocket,顾问销售类似于Socket。
每次服务器调用accept,都会产生一个新的Socket对象(也就是上述买房子的一对一顾问销售)
启动多个客户端方法
客户端:
public class TcpEchoClient {
private Socket socket;
public TcpEchoClient(String serverIpP, int serverPort) throws IOException {
//这里写入IP和端口号后,意味着,new好对象之后和服务器的连接就建立完成了,如果建立连接失败,就会直接在构造对象时抛出异常
socket = new Socket(serverIpP,serverPort);
}
public void start() {
System.out.println("客户端启动!");
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner scanner = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
Scanner scannerIn = new Scanner(System.in);//此处是用户进行输入,补药将里面的参数写成inputStream了
//System.in System.out System.error 这三个特殊的流对象的生命周期是跟随进程的结束而结束,不需要手动close
while(true) {
//1.从控制台读取数据
System.out.print("-> ");
String request = scannerIn.next();
//2.把请求发送给服务器
printWriter.println(request);//把请求放在内存的缓冲区里,如果此处的数据比较少,没办法堆积够足够的数据,则不会进行IO操作,这些数据也只会一直停留在缓存区中,出不去
//PrintWriter这样的类,以及很多IO流中的类,都是自带缓存区的,引入缓存区后,进行写入操作,不会直接触发IO,而是先放到缓存区
//等到混存取里面攒了一波之后,再统一进行发送
printWriter.flush();
//3.从服务器中获取响应
if(!scanner.hasNext()) {
break;
}
String response = scanner.next();
//4.打印响应结果
System.out.println(response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1",9090);
tcpEchoClient.start();
}
}
服务器:
public class TcpEchoServer {
private ServerSocket serverSocket;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
ExecutorService service = Executors.newCachedThreadPool();
while(true) {
//TCP建立连接的流程是在操作系统内核中进行完成的,代码无法感知
//accept操作,是内核已经完成了连接建立的操作后,才能够执行功能
Socket clientSocket = serverSocket.accept();
//如果要让多个客户端使用这个服务器,就需要使用多线程,因为在这里写的代码,是两个while死循环,此时如果进入第一个while循环后
//就会进入processConnection中,如果此时里面的循环没有进行accept,此时外面的while循环就卡住了, 并且也只有里面的循环执行结束后(也就是客户端关闭后)才会执行下一个客户端发起的请求和建立新的客户端连接
/*Thread t = new Thread(() -> {
try {
processConnection(clientSocket);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
t.start();*/
//一旦短时间内有大量的客户端,并且每个客户端请求都是很快的,这个时候对于服务器的开销就会比较有压力,此时就需要用到线程池。
//线程池使用来解决短时间内有大量的客户端,并且客户端发送一个请求之后就快速的断开连接了
service.submit(() -> {
try {
processConnection(clientSocket);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
//但是多线程/线程池不适用于客户端持续的发送请求处理响应,这种连接会保持很久。解决方式就是IO多路复用
//IO多路复用简单地说就是一个人买三个饭,去第一个档口选好饭后,就去第二个档口选饭,然后再去第三个档口,此时前两个档口的的老板会说饭好了,就去拿,这样就是简单的IO多路复用
}
}
private void processConnection(Socket clientSocket) throws IOException {
//先打印客户端信息
System.out.printf("[%s:%d] 客户端上线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
Scanner scanner = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
while (true) {
//1.读取请求并解析
if (!scanner.hasNext()) {
//如果scanner无法读出数据,说明客户端关闭了连接,导致服务器这边读取到”末尾“
break;
}
//scanner.hasNext()和scanner.next() 读取请求数据的时候,请求数据应该以“空白符”结尾(不限于空格,回车,制表符,垂直制表符,翻页符......)
//因此使用\n作为请求和响应的标志
String request = scanner.next();
//2.根据请求计算响应
String response = process(request);
//3.把响应写回客户端
//此处可以按照字节数组直接来写,也可以有另外一种写法
//outputStream.write(response.getBytes());
printWriter.println(response);
printWriter.flush();
//4.打印日志
System.out.printf("[%s:%d] request = %s, response = %s\n",clientSocket.getInetAddress(),clientSocket.getPort()
,request,response);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
System.out.printf("[%s:%d] 客户端下线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
//这里使用close,是因为Socket是“连接级别”的数据,随着客户端断开连接,这个Socket就不再使用了,即使是同一个客户端,断开连接后,重新连接
//也是一个新的socket,原先的socket不是同一个,为了防止文件资源的泄露,就必须主动关闭socket
//之前的ServerSocket和DatagramSocket不手动关闭,是因为它们的生命周期都是跟随整个进程的。
clientSocket.close();
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);
tcpEchoServer.start();
}
}
3.长连接和短连接
(1)长连接
客户端连上服务器之后,一个连接中,会发起多次请求,接受多个响应(一个连接到底要进行多少次请求是不确定的)当前的echo client就是这样的
(2)短连接
客户端连接上服务器之后,一个连接,只发一个请求,接受一个响应,然后就断开连接了。