欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

【JavaWeb学习】socket通信

程序员文章站 2022-07-08 09:40:14
...

ServerSocket用法详解

在B/S通信模式中,服务端需要创建监听特定端口的ServerSocket,ServerSocket负责接收客户的连接请求。

构造ServerSocket

serverSocket的构造函数有四种

  • ServerSocket() throws IOException
  • ServerSocket(int port) throws IOException
  • ServerSocket(int port, int backlog) throws IOException
  • ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException

其中参数port用来绑定端口号(即服务器监听的端口),

参数backlog显示设置连接请求队列的长度,当服务器运行时候,会监听多个客户的连接请求,当服务器端收到多个连接请求,操作系统会把这些连接请求存储到一个先进先出的队列中。许多操作系统限定了队列的最大长度,一般为50。

参数bindAddr,当主机有多个IP地址(多网卡)的时候,就需要显示指定那个IP地址。

上面有一个默认构造方法,它的作用是,允许服务器在绑定到特定的端口之前,先设置ServerSocket的一些选项,因为一旦服务器与特定的端口绑定,有些选项就不能再改变了。

例如,可以先设置ServerSocket的SO_REUSEADDR的选项为true,然后再把它与8888端口绑定

serverSocket.setReuseAddress(true);
serverSocket.bind(new InetSocketAddress(8888));

serverSocket选项

  • SO_TIMEOUT:等待客户连接的超时时间
  • SO_REUSEADDR:表示是否允许重用服务器所绑定的地址,boolean
  • SO_RCVBUF:表示接受数据的缓冲区的大小

SO_TIMEOUT表示serverSocket的accept()方法等待客户端连接的超时时间,以毫秒为单位。如果SO_TIMEOUT的值为0,表示永远不会超时,这是SO_TIMEOUT的默认值。accept方法会一直阻塞直到有客户端连接,方法才返回,或者超出了超时时间,那么accept方法会抛出SocketTimeoutException.

SO_REUSEADDR=false,当serverSocket关闭时,如果网络上还有发送到这个ServerSocket的数据,这个ServerSocket不会立刻释放本地端口,而是会等待一段时间,确保接收到了网络上发送过来的延迟数据,然后再释放端口。许多服务器程序都使用固定的端口,当服务器关闭时,它的端口可能还会被占用一段时间.如果立刻释放端口,释放的端口可能会立刻连上新的应用程序,这样存活在网络中的TCP报文会与新的TCP连接报文冲突,造成数据冲突。需要耐心等待网络中老的TCP连接的活跃报文全部消失,2MSL时间可以满足要求,这里涉及到TCP的四次挥手。

单线程Socket实例

创建ServerSocket服务器

public class Server {
	
	@SuppressWarnings("resource")
	public static void main(String[] args) throws IOException {
		ServerSocket serverSocket=new ServerSocket(8888);//监听8888端口
		while(true) {
			System.out.println("socket已连接,等待客户端请求...");
			Socket socket = serverSocket.accept();
			new WebAction(socket).service();//获取请求内容并响应
		}
	}
}

WebAction类用来获取请求的内容并响应给浏览器

public class WebAction {
	
	private Socket socket;
	
	private BufferedReader bufferedReader=null;
	
	private BufferedWriter bufferedWriter=null;

	public WebAction(Socket socket) {
		this.socket=socket;
	}

	public void service() {
		try {
			readRequest(socket);
			writeHtml(socket);
			close();
		} catch (IOException e) {
		}	
	}
	
	public void readRequest(Socket socket) throws IOException {
		StringBuffer sb=null;
		String temp=null;
		bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));//读取请求的内容
		sb=new StringBuffer();
        System.out.println("---------------------");
        while((temp=bufferedReader.readLine())!=null) {//注意这里,如果客户端不关闭,服务器就会一直等待
        	sb.append(temp);
        	System.out.println(temp);
        }
	}
	
	public void writeHtml(Socket socket) throws IOException {
		
		bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
		StringBuffer sb=new StringBuffer();
		sb.append("http/1.1 200 ok").append("\n\n");
		sb.append("success");
		bufferedWriter.write(sb.toString());
		bufferedWriter.flush();
		bufferedWriter.close();
	}
	
	public void close() {
		try {
			bufferedReader.close();
			bufferedWriter.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

启动服务器,并在浏览器中输入 http://localhost:8888/app?name=zhangsan ,发现请求的内容服务器已经获取到了
【JavaWeb学习】socket通信
但是浏览器一直在等待,这是怎么回事呢
【JavaWeb学习】socket通信
原来如果客户端打开一个输入流,如果不做约定,也不关闭它,那么服务端永远不知道客户端是否发送完消息,那么服务端就会一直等待下去,直到读取超时。所以怎么告诉服务端已经发送完消息就很重要。

socket判断发送完成的方式

  • 方法一:Socket关闭,当socket关闭,服务端会接收到响应的关闭信号,那么服务端就知道流已经关闭了,这个时候读操作完成。但是这种方法太暴力,而且socket关闭后,客户端将不能接收服务器发送的消息,也不能再次发送消息了。如果客户端想再次发送消息,需要重新创建Socket连接。

  • 方法二:客户端发送完成后,调用socket.shutdownOutput()而不是

outputStream.close() //如果关闭了输出流,那么相应的socket也会关闭,和直接调用socket.close是一样的。

这种方法优点是发送完成之后可以接收服务端返回的数据,缺点是也不能再次发送了。

  • 方法三:客户端与服务器约定符号

例如,客户端在发送消息的最后加上一个“end”标志,当服务端读取到该标志,就知道客户端已经发送完成了。但这种方法容易引起误操作。

  • 方法四:客户端指定长度

可以采用计算机界普遍采用的,先通过前面几个字节来说明后面跟随的消息的长度。例如0xxxxxxx

表示第一个字节表示内容的长度,内容最大是128个字节,即128B

话说回来,通过浏览器访问socket服务器,服务器如何知道发送结束了呢?因为浏览器采用的是http协议,HTTP请求包括了一下内容:一个请求行、若干个请求头、实体内容,以GET请求为例,请求格式如下图所示
【JavaWeb学习】socket通信
从上图可知,get请求的最后会有一个空行,我们可以以此作为get请求发送结束的标志。

只要修改一行代码即可

	public void readRequest(Socket socket) throws IOException {
		StringBuffer sb=null;
		String temp=null;
		bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
		sb=new StringBuffer();
        System.out.println("---------------------");
        while((temp=bufferedReader.readLine())!=null && !"".equals(temp)) {//判断get请求是否结束
        	sb.append(temp);
        	System.out.println(temp);
        }
	}

但是上面的例子是单线程的,如果同时有多个请求,那只能排队等待,这显然不太合适,下面我们就改造成多线程的Socket服务器,每当有新的请求,就分配一个线程给该请求。

多线程服务器

为了让服务器能够同时为多个客户提供服务,提高服务器的并发能力,可以创建一个线程池,每次从线程池中取出工作线程为客户服务。

public class Server {
	
	@SuppressWarnings("resource")
	public static void main(String[] args) throws IOException {
		ServerSocket serverSocket=new ServerSocket(8888);
		ExecutorService executorService = Executors.newCachedThreadPool();
		while(true) {
			System.out.println("socket已连接,等待客户端请求...");
			final Socket socket = serverSocket.accept();
			// 每个请求分配一个线程
			executorService.execute(new Runnable() {
				public void run() {
					new WebAction(socket).service();//获取请求内容并响应
				}
			});
		}
	}
}

参考文章

Java Socket变成基础及深入讲解