【JavaWeb学习】socket通信

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 ,发现请求的内容服务器已经获取到了
在这里插入图片描述
但是浏览器一直在等待,这是怎么回事呢
在这里插入图片描述
原来如果客户端打开一个输入流,如果不做约定,也不关闭它,那么服务端永远不知道客户端是否发送完消息,那么服务端就会一直等待下去,直到读取超时。所以怎么告诉服务端已经发送完消息就很重要。

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

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

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

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

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

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

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

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

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

话说回来,通过浏览器访问socket服务器,服务器如何知道发送结束了呢?因为浏览器采用的是http协议,HTTP请求包括了一下内容:一个请求行、若干个请求头、实体内容,以GET请求为例,请求格式如下图所示
在这里插入图片描述
从上图可知,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变成基础及深入讲解

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值