参考资料:
《Java网络编程精解》 孙卫琴
一、socket通信简介
什么是socket,简单来说,在linux系统上,进程与进程之间的通信称为IPC,在同一台计算机中,进程与进程之间通信可以通过信号、共享内存的方式等等。
不同计算机上的进程要进行通信的话就需要进行网络通信,而 socket通信就是不同计算机进程间通信中常见的一种方式,当然,同一台计算机也可以通过socket进行通信,比如mysql支持通过unix socket本地连接。
socket在网络系统中拥有以下作用:
(1) socket屏蔽了不同网络协议之间的差异
(2) socket是网络编程的入口,它提供了大量的系统调用system call供程序员使用
(3) linux的重要思想-一切皆文件,socket也是一种特殊的文件,网络通信在linux系统上同样是对文件的读 写操作
linux上支持多种套接字种类,不同的套接字种类称为"地址簇",这是因为不同的套接字拥有不同的寻址方法。
linux将其抽象为统一的BSD套接字接口,从而屏蔽了它们的区别,程序员关心了只是BSD套接字接口而已。
以INET套接字为例:
Linux在利用socket()进行系统调用时,需要传递套接字的地址族标识符、套接字类型以及协议、源代码:
asmlinkage long sys_socket(int family, int type, int protocol) { int retval; struct socket *sock; retval = sock_create(family, type, protocol, &sock); if (retval < 0) goto out; retval = sock_map_fd(sock); if (retval < 0) goto out_release; out: /* It may be already another descriptor 8) Not kernel problem. */ return retval; out_release: sock_release(sock); return retval; }
不过对于用户而言,socket就是一种特殊的文件而已....
二、TCP/IP以及SOCKET通信简介
linux上网络通信实现由通信子网和资源子网2部分,
通信子网位于linux内核空间,由linux内核实现,例如netfilter, tcp/ip协议栈等等功能
资源子网由位于用户空间的程序实现,例如httpd, nginx, haproxy等等。
计算机通信本质上是进程间的通信,一个计算机上可能运行着多个进程,我们使用端口来标记一个唯一的进程.
0~1023:管理员才有权限使用,永久地分配给某应用使用;
注册端口:1024~41951:只有一部分被注册,分配原则上非特别严格;
动态端口或私有端口:41952+:
tcp实现了以下功能:
①连接建立
②将数据打包成段 MTU通常为1500以下
校验和
③确认、重传以及超时机制
④排序
序列号 32位 并非从0开始 过大的话循环轮换 从0开始
⑤流量控制 速度不同步2台数据的服务器 防止阻塞
缓冲区 发送缓冲 接收缓冲
滑动窗口
⑥拥塞控制 多个进程通信
慢启动 通过慢启动的方式探测,启动的时候很小 随后以指数级增长。
拥塞避免算法
tcp是一个有限状态机,三次连接,四次握手:
注意:如果server端没有调用close()方法,可能出现大量连接处于CLOSE_WAIT状态,占用系统资源。
三、Socket用法
在C/S通信模式中,客户端主动创建与服务器连接的Socket,服务器收到了客户端的连接请求,也会创建与客户端连接的Socket。
Socket是通信连接两端的收发器。服务器端监听在某个固定的端口上,每当有一个客户端连入时,都要创建一个socket文件,因此,linux系统打开文件数量直接影响着服务器端socket通信的并发能力。
3.1 构造器
当客户端创建Socket连接Server时,会随机分配端口,因此不用指定
public static void main(String[] args) throws Exception{ Socket socket = new Socket(); //远程服务器地址 SocketAddress remoteAddr = new InetSocketAddress("localhost",8000); //设定超时时长,单位ms,为0表示永不超时,超时则跑出SocketTimeoutException socket.connect(remoteAddr,60*1000); }
设定客户端地址:
在一个Socket对象中,同时包含了远程服务器的ip地址,端口信息,也要包含客户端的ip地址和端口信息,才能进行双向通信。
默认,客户端不设置ip的话,客户端地址就是当前客户端主机的地址。构造器中支持显式指定。
Socket的创建和连接中出现的各种异常说明:
(1) UnkownHostException
无法识别主机名或者ip地址,找不到server主机
(2) ConnectException
2种情况:
没有服务器进程监听该端口
服务器进程拒绝连接:比如服务器端设置了请求队列长度等情形。
(3) SocketTimeoutException
连接超时
(4) BindException
无法把Socket对象和指定的本地IP地址或者端口绑定,就会抛出这种异常
例如:socket.bind(new InetSocketAddress.getByName("222.34.5.7"),1234);
有可能本地主机没有改地址,或者该端口不能被使用,就会抛出该异常。
3.2 获取Socket信息
Socket包含了连接的相关信息,client和server的地址端口等等,还可以获取InputStream和OutputStream,以下是一个demo
public class HTTPClient { String host="www.javathinker.org"; int port=80; Socket socket; public void createSocket()throws Exception{ socket=new Socket("www.javathinker.org",80); } public void communicate()throws Exception{ StringBuffer sb=new StringBuffer("GET "+"/index.jsp"+" HTTP/1.1\r\n"); sb.append("Host: www.javathinker.org\r\n"); sb.append("Accept: */*\r\n"); sb.append("Accept-Language: zh-cn\r\n"); sb.append("Accept-Encoding: gzip, deflate\r\n"); sb.append("User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)\r\n"); sb.append("Connection: Keep-Alive\r\n\r\n"); //发出HTTP请求 OutputStream socketOut=socket.getOutputStream(); socketOut.write(sb.toString().getBytes()); socket.shutdownOutput(); //关闭输出流 //接收响应结果 InputStream socketIn=socket.getInputStream(); ByteArrayOutputStream buffer=new ByteArrayOutputStream(); byte[] buff=new byte[1024]; int len=-1; while((len=socketIn.read(buff))!=-1){ buffer.write(buff,0,len); } System.out.println(new String(buffer.toByteArray())); //把字节数组转换为字符串 /* InputStream socketIn=socket.getInputStream(); BufferedReader br=new BufferedReader(new InputStreamReader(socketIn)); String data; while((data=br.readLine())!=null){ System.out.println(data); } */ socket.close(); } public static void main(String args[])throws Exception{ HTTPClient client=new HTTPClient(); client.createSocket(); client.communicate(); } }
说明:上面方法用ByteArrayOutputStream来接收响应信息,也就是说响应会全部放置在内存中,在响应报文很长的时候这样很不明智,上面注释的代码中演示了如何使用BufferReader逐行进行读取。
3.3 关闭Socket
网络通信占用资源且有太多的因素,在finally代码块中关闭socket是省事的
Socket类提供了3个状态测试方法:
isClosed(): 如果Socket已经连接到远程主机,并且还没有关闭,则返回true
isConnected(): 如果Socket曾经连接到过远程主机,返回true
isBound(): 如果Socket和本地端口绑定,返回true
因此确定一个Socket对象正在处于连接状态,可以用以下方式
boolean isConnected = socket.isConnected() && !socket.isClosed();
3.4 半关闭Socket
socket通信也就是2个进程之间的通信,无论这2个进程是否处于同一个物理机器上,只需要向内核申请注册了端口就可以用ip+port进行唯一的标识。
假设2个进程A和B之间通信,A如何通知B所有数据已经传输完毕呢?
以上文中HttpClient为例
StringBuffer sb=new StringBuffer("GET "+"/index.jsp"+" HTTP/1.1\r\n"); sb.append("Host: www.javathinker.org\r\n"); sb.append("Accept: */*\r\n"); sb.append("Accept-Language: zh-cn\r\n"); sb.append("Accept-Encoding: gzip, deflate\r\n"); sb.append("User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)\r\n"); sb.append("Connection: Keep-Alive\r\n\r\n");
这实际上是典型的HTTP处理的方式,没有请求实体,因此以\r\n\r\n表示结束,这就是一种约定方式。
(1) 如果是字符流,可以以特殊字符作为结束标志,可以是\r\n\r\n,甚至于可以定义为"bye"
(2) A可以先发送一个消息,事先声明了内容长度
(3) A发送完毕之后,主动关闭Socket,B读取完了所有数据也关闭
(4) shutdownInput, shutdownOutput 之关闭输出流或者输出流,但是这并不会释放资源,必须调用Socket的close()方法,才会释放资源
3.5 Socket常用选项
TCP_NODELAY: 表示立即发送数据,默认是false,表示开启Negale算法,true表示关闭缓冲,确保数据及时发送
为false时,适合发送方需要发送大批量数据,并且接收方及时响应,这种算法通过减少传输数据的次数来提高效率
为true,发送方持续的发送小批量数据,并且接受方不一定会立即响应数据
SO_REUSEADDR: 表示是否允许重用Socket绑定的本地地址
SO_TIMEOUT: 表示接收数据的等待超时时间
SO_LINGER: 表示执行Socket的close()方法时,是否立即关闭底层的Socket,哪怕还有数据没有发送完也直接关闭
SO_SNFBUF: 发送方缓冲区大小
SO_RCVBUF: 接收数据的缓冲区大小
SO_KEEPALIVE: 对于长时间处于空闲状态的Socket是否要自动关闭
四、ServerSocket用法
在C/S架构中,服务器端需要创建监听特定端口的ServerSocket,ServerSocket负责接收客户的连接请求。
4.1 ServerSocket
1.必须绑定一个端口
ServerSocket serverSocket = new ServerSocket(80);
如果无法绑定到一个端口,会抛出BindException,一般由以下原因:
(1) 端口已经被占用
(2) 某些操作系统中,只有超级用户才允许使用1-1023的端口
如果port设置为0,表示操作系统来分配一个任意可用的端口,匿名端口,在某些场合,匿名端口有特殊作用
2. 设定客户连接请求队列的长度
一般的C/S架构中,服务器监听在某个固定的端口上,每来一个客户端连接,服务器都会创建一个socket文件维护与client的通信
管理client连接的任务往往由操作系统来完成。操作系统把这些连接请求存储在一个先进先出的队列中。
许多操作系统限定了队列的最大长度,一般是50。当client connections>50 时,服务器会拒绝新的请求。
对于客户端而言,如果他的请求被server加入了队列,意味着连接成功,这个队列通常称为backlog.
ServerSocket构造方法的backlog参数用来显示指定连接请求队列的长度,它将覆盖操作系统限定的最大长度,不过在以下情形,依旧采用操作系统的默认值:
(1) backlog <= 0
(2) without setting backlog
(3) backlog参数的值 > 操作系统的允许范围
演示: Server端设置backlog为3,不处理请求,client连接超过3会拒绝
import java.io.*; import java.net.*; public class Server { private int port=8000; private ServerSocket serverSocket; public Server() throws IOException { serverSocket = new ServerSocket(port,3); //连接请求队列的长度为3 System.out.println("服务器启动"); } public void service() { while (true) { Socket socket=null; try { socket = serverSocket.accept(); //从连接请求队列中取出一个连接 System.out.println("New connection accepted " + socket.getInetAddress() + ":" +socket.getPort()); }catch (IOException e) { e.printStackTrace(); }finally { try{ if(socket!=null)socket.close(); }catch (IOException e) {e.printStackTrace();} } } } public static void main(String args[])throws Exception { Server server=new Server(); Thread.sleep(60000*10); //睡眠十分钟 //server.service(); } }
import java.net.*; public class Client { public static void main(String args[])throws Exception{ final int length=100; String host="localhost"; int port=8000; Socket[] sockets=new Socket[length]; for(int i=0;i<length;i++){ //试图建立100次连接 sockets[i]=new Socket(host, port); System.out.println("第"+(i+1)+"次连接成功"); } Thread.sleep(3000); for(int i=0;i<length;i++){ sockets[i].close(); //断开连接 } } }
3. 设定绑定的IP地址
一个主机可能有多个地址,此时可以显示指定
ServerSocket serverSocket = new ServerSocket(); // 只有在设定地址之前设置才有效 serverSocket.setReuseAddress(true); serverSocket.bind(new InetSocketAddress(8000));
4. 关闭ServerSocket
同样应该在finally代码块中调用close()方法,在一般的连接中,往往是由客户端发起请求,也是由客户端发起关闭socket请求。
但是,在某些keepalive的场景中,例如httpd,nginx等等服务器都支持长连接,通过设定keepalive的最大连接时长和最大连接数来控制长连接。
此时,那些由于超时的client连接,服务器端会主动发起close()请求。
如何判断ServerSocket没有关闭
boolean isOpen = serverSocket.isBound() && !serverSocket.isClosed();
4.2 ServerSocket选项
1. SO_TIMEOUT
accept()方法等待客户端的连接超时时间,以ms为单位,0表示永不超时,默认是0.
当执行accept()时,如果backlog为空,则服务器一直等待,如果设置了超时时间,则服务器端阻塞在此,超时则抛出SocketTimeoutException
2. SO_REUSEADDR选项
当服务器因为某些原因需要重启时,如果网络上还有发送到这个ServerSocket的数据,则ServerSocket不会立刻释放该端口,导致重启失败。
设置为true的话可以确保释放,但是必须在绑定端口之前调用方法。
3. SO_RCVBUF
接收缓冲大小
五、Demo
import java.io.*; import java.net.*; import java.util.concurrent.*; public class EchoServer { private int port=8000; private ServerSocket serverSocket; private ExecutorService executorService; //线程池 private final int POOL_SIZE=4; //单个CPU时线程池中工作线程的数目 private int portForShutdown=8001; //用于监听关闭服务器命令的端口 private ServerSocket serverSocketForShutdown; private boolean isShutdown=false; //服务器是否已经关闭 private Thread shutdownThread=new Thread(){ //负责关闭服务器的线程 public void start(){ this.setDaemon(true); //设置为守护线程(也称为后台线程) super.start(); } public void run(){ while (!isShutdown) { Socket socketForShutdown=null; try { socketForShutdown= serverSocketForShutdown.accept(); BufferedReader br = new BufferedReader( new InputStreamReader(socketForShutdown.getInputStream())); String command=br.readLine(); if(command.equals("shutdown")){ long beginTime=System.currentTimeMillis(); socketForShutdown.getOutputStream().write("服务器正在关闭\r\n".getBytes()); isShutdown=true; //请求关闭线程池 //线程池不再接收新的任务,但是会继续执行完工作队列中现有的任务 executorService.shutdown(); //等待关闭线程池,每次等待的超时时间为30秒 while(!executorService.isTerminated()) executorService.awaitTermination(30,TimeUnit.SECONDS); serverSocket.close(); //关闭与EchoClient客户通信的ServerSocket long endTime=System.currentTimeMillis(); socketForShutdown.getOutputStream().write(("服务器已经关闭,"+ "关闭服务器用了"+(endTime-beginTime)+"毫秒\r\n").getBytes()); socketForShutdown.close(); serverSocketForShutdown.close(); }else{ socketForShutdown.getOutputStream().write("错误的命令\r\n".getBytes()); socketForShutdown.close(); } }catch (Exception e) { e.printStackTrace(); } } } }; public EchoServer() throws IOException { serverSocket = new ServerSocket(port); serverSocket.setSoTimeout(60000); //设定等待客户连接的超过时间为60秒 serverSocketForShutdown = new ServerSocket(portForShutdown); //创建线程池 executorService= Executors.newFixedThreadPool( Runtime.getRuntime().availableProcessors() * POOL_SIZE); shutdownThread.start(); //启动负责关闭服务器的线程 System.out.println("服务器启动"); } public void service() { while (!isShutdown) { Socket socket=null; try { socket = serverSocket.accept(); //可能会抛出SocketTimeoutException和SocketException socket.setSoTimeout(60000); //把等待客户发送数据的超时时间设为60秒 executorService.execute(new Handler(socket)); //可能会抛出RejectedExecutionException }catch(SocketTimeoutException e){ //不必处理等待客户连接时出现的超时异常 }catch(RejectedExecutionException e){ try{ if(socket!=null)socket.close(); }catch(IOException x){} return; }catch(SocketException e) { //如果是由于在执行serverSocket.accept()方法时, //ServerSocket被ShutdownThread线程关闭而导致的异常,就退出service()方法 if(e.getMessage().indexOf("socket closed")!=-1)return; }catch(IOException e) { e.printStackTrace(); } } } public static void main(String args[])throws IOException { new EchoServer().service(); } } class Handler implements Runnable{ private Socket socket; public Handler(Socket socket){ this.socket=socket; } private PrintWriter getWriter(Socket socket)throws IOException{ OutputStream socketOut = socket.getOutputStream(); return new PrintWriter(socketOut,true); } private BufferedReader getReader(Socket socket)throws IOException{ InputStream socketIn = socket.getInputStream(); return new BufferedReader(new InputStreamReader(socketIn)); } public String echo(String msg) { return "echo:" + msg; } public void run(){ try { System.out.println("New connection accepted " + socket.getInetAddress() + ":" +socket.getPort()); BufferedReader br =getReader(socket); PrintWriter pw = getWriter(socket); String msg = null; while ((msg = br.readLine()) != null) { System.out.println(msg); pw.println(echo(msg)); if (msg.equals("bye")) break; } }catch (IOException e) { e.printStackTrace(); }finally { try{ if(socket!=null)socket.close(); }catch (IOException e) {e.printStackTrace();} } } }