【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();//获取请求内容并响应
}
});
}
}
}
参考文章
下一篇: 1103:分糖果II