Socket编程注意点:
Socket connect(SocketAddress endpoint, int timeout)方法:连接到服务器,并指定一个超时值。超时值零被解释为无限超时。在建立连接或者发生错误之前,这个方法一直处于阻塞状态。【在编写程序时这个超时值最好设置上】
Socket setSoTimeout方法:Socket的read方法是一个阻塞方法,setSoTimeout设置阻塞的时间,如果超过阻塞时间会引发SocketTimeoutException;
通过Socket的输入流读取数据:是阻塞的;一般会在流的外面套一个BufferInputStream,通过read(byte[] b)和read(b, 0, b.length)方法读取指定长度的字节,通过结合包头中指定的包体长度可以解决粘包问题。read(byte[] b)和read(b, 0, b.length),通过重复地调用底层流的read方法,尝试读取尽可能多的字节。这种迭代的read会一直继续下去,直到满足一下条件之一:
·已经读取了指定的字节数
·底层流的read方法返回-1,指示文件末尾
·底层流的available方法返回0,指示将阻塞后续的输入请求
Socket isClosed()
Socket isConnected()
其他设置项
Socket setOOBInline(boolean on):启用/禁用OOBINLINE(TCP紧急数据的接收)
Socket setSendBufferSize(int size)
Socket setReceiveBufferSize(int size)
Socket setKeepAlive(boolean on):一种心跳机制,但是不见使用。
Socket setTrafficClass(int tc)
Socket setReuseAddress(boolean on)
Socket setTcpNoDelay(boolean on)
Socket setSoLinger(boolean on, int linger):设置在关闭套接字时,是否立即返回(不等待底层数据发送完毕)
例子:实现基本的通讯(发包解包)、请求、心跳机制
服务端代码
package com.tongxun.server; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * 服务器端Socket * @author Administrator */ public class SocketServer { private MapsocketMap = new ConcurrentHashMap (); private ServerSocket server; // 服务器端Socket对象 private int port;//服务端端口 private int timeout;//连接维持超时 /** * 初始化服务器 * @param port * @param timeout */ public SocketServer(int port, int timeout) { if(port<=0 || timeout<=0) throw new IllegalArgumentException(); this.port = port; this.timeout = timeout; } /** * 启动服务器socket */ public void start(){ try { // 创建一个ServerSocket在端口2121监听客户请求 server = new ServerSocket(this.port); System.out.println("Server starts..."); } catch (Exception e) { System.out.println("Can not listen to. " + e); } //接收客户端连接 while (true) { try { acceptConn(); } catch (IOException e) { e.printStackTrace(); } } } /** * 接收客户端连接 * @throws IOException */ private void acceptConn() throws IOException{ Socket socket = server.accept(); socket.setSoTimeout(1000); StringBuffer sb = new StringBuffer(); sb.append(socket.getInetAddress().getHostAddress()); sb.append(":"); sb.append(socket.getPort()); socketMap.put(sb.toString(), socket); new SRecvThread(socket, this).start(); } public int getTimeout() { return timeout; } /** * 主方法 * @param args */ public static void main(String[] args) { SocketServer server = new SocketServer(2121, 10000); server.start(); } }
package com.tongxun.server; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.IOException; import java.net.Socket; import java.net.SocketTimeoutException; import java.text.MessageFormat; import com.tongxun.util.Message; import com.tongxun.util.PackageHead; /** * 接收线程 * @author zhangjc */ public class SRecvThread extends Thread { private Socket socket; // 保存与本线程相关的Socket对象 private BufferedInputStream inputStream; private BufferedOutputStream outputStream; private byte[] head = new byte[10]; private long lastRecv = System.currentTimeMillis();//最近一次接收时间 private boolean running = true; private BusiHandler handler; private SocketServer context; public SRecvThread(Socket socket, SocketServer context) { this.socket = socket; try { this.context = context; inputStream = new BufferedInputStream(this.socket.getInputStream(), 1024); outputStream = new BufferedOutputStream(this.socket.getOutputStream(), 1024); handler = new BusiHandler(outputStream); System.err.println(MessageFormat.format("客户端{0}登录系统", this.getConnInfo())); } catch (IOException e) { e.printStackTrace(); close(); } } public void run() { try { while (running) { recv(); } } catch (Exception e) { e.printStackTrace(); this.close(); } } /** * 连接关闭和收尾动作 */ private void close(){ try { running = false; if(inputStream!=null) inputStream.close(); if(outputStream!=null) outputStream.close(); if(!socket.isConnected()) socket.close(); System.out.println("连接断开"); } catch (IOException e) { e.printStackTrace(); } } private String getConnInfo(){ return this.socket.getInetAddress().getHostAddress()+":"+this.socket.getPort(); } /** * 接收处理 * @return * @throws Exception */ private Message recv() throws Exception{ Message msg = new Message(); try { read(head); PackageHead headPack = new PackageHead(head); byte[] body = new byte[headPack.getLen()]; if(headPack.getLen()>0){ inputStream.read(body); } msg.setHead(headPack); msg.setBody(body); this.lastRecv = System.currentTimeMillis(); handler.exe(msg); } catch (SocketTimeoutException e) { if(System.currentTimeMillis()-lastRecv >= context.getTimeout()){ System.out.println(System.currentTimeMillis()-lastRecv); System.out.println("超时"); this.close(); } } return msg; } /** * 读取buff长度的数据,如果len为-1则表示连接断开了 * @param buff * @return * @throws Exception */ private int read(byte[] buff) throws Exception{ int len = inputStream.read(buff); if(len==-1) throw new Exception("客户端连接断开"); return len; } }
package com.tongxun.server; import java.io.BufferedOutputStream; import java.io.IOException; import com.tongxun.util.Message; import com.tongxun.util.PackageHead; /** * 业务处理 * @author zhangjc */ public class BusiHandler { private BufferedOutputStream outputStream; public BusiHandler(BufferedOutputStream outputStream){ this.outputStream = outputStream; } public void exe(Message msg) throws IOException{ //心跳包处理 System.out.println(msg.getHead()); if(msg.getHead().getType() == 2){ System.out.println("接收心跳"); PackageHead retHead = new PackageHead(); Message retMsg = new Message(); retHead.setType((short)2); retMsg.setHead(retHead); retMsg.setBody(new byte[0]); outputStream.write(retMsg.toBytes()); outputStream.flush(); } //时间请求处理 if(msg.getHead().getType() == 1){ System.out.println("接收时间请求"); PackageHead retHead = new PackageHead(); Message retMsg = new Message(); byte[] retBody = String.valueOf(System.currentTimeMillis()).getBytes(); retHead.setType((short)1); retHead.setLen((short)retBody.length); retMsg.setHead(retHead); retMsg.setBody(retBody); outputStream.write(retMsg.toBytes()); outputStream.flush(); } } }
客户端代码
package com.tongxun.client; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.IOException; import java.net.InetSocketAddress; import java.net.Socket; import com.tongxun.util.Message; import com.tongxun.util.PackageHead; /** * 客户端Socket * @author Administrator */ public class SocketClient { private int port;//服务器端口 private String address;//服务器地址 private int connTimeout;//建立连接时超时 private int timeout;//连接维持超时 private final int interval = 5000;//心跳包发送间隔 private final int minTimeout = 9000;//最小超时时间 private CRecvThread recvThread;//接收线程 private HearbeatThread hearbeatThread;//心跳包发送线程 private Socket socket; private BufferedInputStream inputStream; private BufferedOutputStream outputStream; /** * socket初始化 * @param address 服务端地址 * @param port 服务器端口 * @param connTimeout 建立连接的超时时间 */ public SocketClient(String address, int port, int connTimeout, int timeout) { socket = new Socket(); if(address == null || port<=0 || connTimeout<=0 || timeout<=0) throw new IllegalArgumentException(); this.address = address; this.port = port; this.connTimeout = connTimeout; this.timeout = timeout; } /** * 建立连接并开启心跳发送线程和接收线程 */ public void start(){ try{ //在建立连接或发生错误之前,一直处于阻塞状态 //所以最好设置一个超时时间 socket.connect(new InetSocketAddress(address, port), connTimeout); //设置read方法最长阻塞时间,超过会触发SocketTimeoutException socket.setSoTimeout(1000); inputStream = new BufferedInputStream(this.socket.getInputStream(), 1024); outputStream = new BufferedOutputStream(this.socket.getOutputStream(), 1024); }catch(Exception e){ //提示用户建立连接出错 System.err.println(e); close(e); } System.err.println("Established a connection..."); //接收线程 recvThread = new CRecvThread(this); recvThread.start(); //心跳发送线程 hearbeatThread = new HearbeatThread(this); hearbeatThread.start(); } /** * 关闭socket并做相应的收尾处理 * @param e */ public void close(Exception e){ if(e!=null) e.printStackTrace(); if(recvThread!=null) recvThread.close(); if(hearbeatThread!=null) hearbeatThread.close(); try { if(inputStream!=null) inputStream.close(); if(outputStream!=null) outputStream.close(); if(socket.isConnected()) socket.close(); System.out.println("连接断开"); } catch (IOException ex) { ex.printStackTrace(); } } /** * 发送时间请求 */ public void sendReq(){ Message msg = new Message(); PackageHead head = new PackageHead(); head.setType((short)1); msg.setHead(head); msg.setBody(new byte[0]); try { outputStream.write(msg.toBytes()); outputStream.flush(); System.out.println("发送请求"); } catch (Exception e) { close(e); } } public BufferedInputStream getInputStream() { return inputStream; } public BufferedOutputStream getOutputStream() { return outputStream; } public int getTimeout() { return timeout; } public int getInterval() { return interval; } public int getMinTimeout() { return minTimeout; } /** * 主方法 * @param args * @throws InterruptedException */ public static void main(String[] args) throws InterruptedException { SocketClient client = new SocketClient("localhost", 2121, 5000, 10000); client.start(); Thread.sleep(5000); client.sendReq(); } }
package com.tongxun.client; import java.net.SocketTimeoutException; import com.tongxun.util.Message; import com.tongxun.util.PackageHead; /** * 回包接收线程 * @author zhangjc */ public class CRecvThread extends Thread { private byte[] head = new byte[10];//用来存放包头的缓存 private long lastRecv = System.currentTimeMillis();//最后一次接收回包的时间 private boolean running = true; private SocketClient context; public CRecvThread(SocketClient context) { this.context = context; } public void run() { try { while (running) { recv(); } } catch (Exception e) { e.printStackTrace(); context.close(e); } } public void close(){ running = false; } private Message recv() throws Exception{ Message msg = new Message(); try { //读取包头 read(head); PackageHead headPack = new PackageHead(head); byte[] body = new byte[headPack.getLen()]; if(headPack.getLen()>0){ read(body); } msg.setHead(headPack); msg.setBody(body); this.lastRecv = System.currentTimeMillis(); //心跳包处理 if(msg.getHead().getType() == 2){ System.out.println("心跳"); } //时间请求回包 if(msg.getHead().getType() == 1){ if(msg.getBody()!=null){ System.out.println(new String(msg.getBody())); } } } catch (SocketTimeoutException e) { //定时触发SocketTimeoutException来判断连接是否超时 if(System.currentTimeMillis()-lastRecv >= context.getTimeout()){ context.close(e); } } return msg; } /** * 读取buff长度的数据,如果len为-1则表示连接断开了 * @param buff * @return * @throws Exception */ private int read(byte[] buff) throws Exception{ int len = context.getInputStream().read(buff); if(len==-1) throw new Exception("服务器连接断开"); return len; } }
package com.tongxun.client; import com.tongxun.util.Message; import com.tongxun.util.PackageHead; /** * 心跳发送线程 * @author zhangjc */ public class HearbeatThread extends Thread { private SocketClient context; private boolean running = true; public HearbeatThread(SocketClient context) { if(context==null){ throw new IllegalArgumentException("context不能为null"); } if(context.getTimeout() <= context.getMinTimeout()){ throw new IllegalArgumentException("超时时间不能小于"+context.getMinTimeout()); } this.context = context; } public void run() { try { sleep(context.getInterval()); while (running) { //组包 Message msg = new Message(); PackageHead head = new PackageHead(); head.setType((short)2); msg.setHead(head); msg.setBody(new byte[0]); //发送 context.getOutputStream().write(msg.toBytes()); context.getOutputStream().flush(); sleep(context.getInterval()); } } catch (Exception e) { context.close(e); } } public void close(){ running = false; } }
工具类
package com.tongxun.util; /** * 基础数据与字节数组的转化 * @author zhangjc */ public class BytesUtil { public static short getShort(byte[] b, int index) { return (short) (((b[index + 1] << 8) | b[index + 0] & 0xff)); } public static void putShort(byte b[], short s, int index) { b[index + 1] = (byte) (s >> 8); b[index + 0] = (byte) (s >> 0); } }
package com.tongxun.util; import java.io.ByteArrayOutputStream; import java.io.IOException; /** * 消息结构 * @author zhangjc */ public class Message { private PackageHead head; private byte[] body; public PackageHead getHead() { return head; } public void setHead(PackageHead head) { this.head = head; } public byte[] getBody() { return body; } public void setBody(byte[] body) { this.body = body; } public byte[] toBytes() throws IOException{ ByteArrayOutputStream array = new ByteArrayOutputStream(); array.write(head.toBytes()); array.write(getBody()); return array.toByteArray(); } }
package com.tongxun.util; /** * 消息头 * 保留4个字节 * @author zhangjc */ public class PackageHead{ private byte[] bytes = new byte[10]; /**长度*/ private short len; /**数据类型*/ private short type; /**目的地*/ private short target; public PackageHead(byte[] bytes){ this.bytes = bytes; this.len = BytesUtil.getShort(bytes, 0); this.type = BytesUtil.getShort(bytes, 2); this.target = BytesUtil.getShort(bytes, 4); } public PackageHead(){} public byte[] getBytes() { return bytes; } public void setBytes(byte[] bytes) { this.bytes = bytes; } public short getLen() { return len; } public void setLen(short len) { this.len = len; } public short getType() { return type; } public void setType(short type) { this.type = type; } public short getTarget() { return target; } public void setTarget(short target) { this.target = target; } public byte[] toBytes(){ byte[] ret = new byte[10]; BytesUtil.putShort(ret, (short)this.len, 0); BytesUtil.putShort(ret, (short)this.type, 2); BytesUtil.putShort(ret, (short)this.target, 4); ret[9] = 124; return ret; } }