java简单的socket编程
前言,背景什么的不会写,要了解背景的,基础tcp/ip知识的百度吧,一大票那种文章,我这边就不写废话了,省得太长不看。
直接讲步骤和代码,有例子是最好理解的。
本篇分成2部分:一是client客户端、二是server服务端
先从服务端server讲起:
①创建ServerSocket对象
serverSocket = new ServerSocket(port);
②监听接入,生成Socket对象
while (true) {
Socket socket = serverSocket.accept();
log.info("client连入成功:" + socket.getLocalPort());
new Thread(new ServerRunnable(socket)).start();
}
③启动对应的socket线程,供socket执行具体的业务逻辑
new Thread(new ServerRunnable(socket)).start();
内部类ServerRunnable:
private class ServerRunnable implements Runnable {
private Socket socket;
private OutputStream writer;
public ServerRunnable(Socket socket) throws IOException {
super();
this.socket = socket;
this.writer = socket.getOutputStream();
}
@Override
public void run() {
// 服务端发送心跳包
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000 * 10 * 3);
if (!socket.isClosed()) {
int tag = CommonUtils.getNum();
queue.put(ServerSocketUtil.sendHeart(tag));
} else {
throw new Exception("socket关闭");
}
} catch (Exception e) {
log.error("client发送心跳包:"+e.getMessage(), e);
break;
}
}
}
}).start();
new Thread(new ServerReadThread(socket, queue)).start();
new Thread(new ServerWriteThread(socket, queue)).start();
}
}
以下是server类完整代码:
public class MyServerSocket {
private Logger log = Logger.getLogger(MyServerSocket.class);
private int port = 17921;
private LinkedBlockingQueue<byte[]> queue = new LinkedBlockingQueue<byte[]>(10);
private ServerSocket serverSocket = null;
public LinkedBlockingQueue<byte[]> getQueue() {
return queue;
}
public void setQueue(LinkedBlockingQueue<byte[]> queue) {
this.queue = queue;
}
public void start() {
new Thread(new Runnable() {
@Override
public void run() {
try {
serverSocket = new ServerSocket(port);
log.info("服务端服务启动监听:" + port);
while (true) {
Socket socket = serverSocket.accept();
log.info("client连入成功:" + socket.getLocalPort());
new Thread(new ServerRunnable(socket)).start();
}
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
}).start();
}
private class ServerRunnable implements Runnable {
private Socket socket;
private OutputStream writer;
public ServerRunnable(Socket socket) throws IOException {
super();
this.socket = socket;
this.writer = socket.getOutputStream();
}
@Override
public void run() {
// 服务端发送心跳包
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000 * 10 * 3);
if (!socket.isClosed()) {
int tag = CommonUtils.getNum();
queue.put(ServerSocketUtil.sendHeart(tag));
} else {
throw new Exception("socket关闭");
}
} catch (Exception e) {
log.error("client发送心跳包:"+e.getMessage(), e);
break;
}
}
}
}).start();
new Thread(new ServerReadThread(socket, queue)).start();
new Thread(new ServerWriteThread(socket, queue)).start();
}
}
}
如果要用spring启动就需要建一个bean标签,将启动方法指定为start。
重点:接下来是读线程和写的线程,读写肯定要分成2个独立的线程,因为简单的socket是bio模式(阻塞io),read流是一个阻塞的流,会阻塞write操作,如果读写不分开,那个write就会被卡组,除非read有值读取,程序才能往下执行。
读线程:
public class ServerReadThread implements Runnable {
private Logger log = Logger.getLogger(ServerReadThread.class);
private BufferedInputStream reader = null;
private LinkedBlockingQueue<byte[]> queue = null;
private Socket socket = null;
/**
* 服务端读取数据类 接收上报数据
*
* @param socket
* @param queue
*/
public ServerReadThread(Socket socket, LinkedBlockingQueue<byte[]> queue) {
super();
this.queue = queue;
this.socket = socket;
try {
this.reader = new BufferedInputStream(socket.getInputStream());
} catch (IOException e) {
log.error("server read thread constructor:" + e.getMessage(), e);
}
}
/**
* 并下发命令
*
* @param lineString
* @throws InterruptedException
*/
private void excuteReceiveDatas(String lineString) throws InterruptedException {
log.info("当期时间:"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())+"---"+lineString);
}
@Override
public void run() {
while (true) {
try {
//按行读取需要将流变成BufferedReader使用readLine方法,这个很简单,就不多赘述了。
// 解析数据包(根据自己业务规定的协议,按字节读取,根据长度解析,避免粘包)
byte[] head = new byte[] { 0, 0, 0, 0 };
reader.read(head, 0, 2);
byte[] len = new byte[] { 0, 0, 0, 0 };
reader.read(len, 0, len.length);
int length = Int2ByteUtil.byteArrayToInt(len);
int surplusLen = length-2-4;//-head-length
//剩余字节长度
byte[] surplusByte = new byte[surplusLen];
reader.read(surplusByte, 0, surplusLen);
/**
* tag 0-3
* cmd 4,5
* ut 6
* s1 7
* s2 8
* s3 9
*/
byte[] tag = new byte[] { 0, 0, 0, 0 };
tag[0]=surplusByte[0];
tag[1]=surplusByte[1];
tag[2]=surplusByte[2];
tag[3]=surplusByte[3];
/**
* 指令 服务端心跳:0x03 客户端心跳回复:0x02 客户端发送消息:0x06
*/
byte[] cmd = new byte[] { 0, 0, 0, 0 };
cmd[0]=surplusByte[4];
cmd[1]=surplusByte[5];
int command = Int2ByteUtil.byteArrayToInt(cmd);
log.info("command:"+command);
byte[] ut = new byte[] { 0, 0, 0, 0 };
ut[0] = surplusByte[6];
// 预留字段
byte[] s1 = new byte[] { 0, 0, 0, 0 };
byte[] s2 = new byte[] { 0, 0, 0, 0 };
byte[] s3 = new byte[] { 0, 0, 0, 0 };
int datalen = length - 0x10;
byte[] data = null;
if (datalen > 0) {
data = new byte[datalen];
reader.read(data, 0, data.length);
}
if (command == 0x05) {
// 客户端发送消息包处理(处理上报数据)
String lineString = new String(data, "utf-8");
excuteReceiveDatas(lineString);
} else if (command == 0x01) {
queue.put(ServerSocketUtil.sendHeartReply(CommonUtils.getNum()));
} else if (command == 0x04) {
// 客户端回应服务端心跳
log.info("客户端收到服务端发送的心跳包");
}
} catch (Exception e) {
log.error("server read thread:" + e.getMessage(), e);
try {
reader.close();reader = null;
socket.close();socket = null;
} catch (IOException e1) {
log.error(e1.getMessage(), e1);
}
break;
}
}
}
}
writer:
public class ServerWriteThread implements Runnable {
private Logger log = Logger.getLogger(ServerWriteThread.class);
private BufferedOutputStream writer = null;
private LinkedBlockingQueue<byte[]> queue = null;
private Socket socket = null;
/**
* 服务端发送数据类 下发命令
*
* @param socket
* @param queue
*/
public ServerWriteThread(Socket socket, LinkedBlockingQueue<byte[]> queue) {
super();
this.queue = queue;
this.socket = socket;
try {
this.writer = new BufferedOutputStream(socket.getOutputStream());
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
@Override
public void run() {
while (true) {
byte[] element = null;
try {
// 防止下次写数据的时候write关闭,将queue元素take出来
if (!socket.isClosed()) {
element = queue.take();
//log.info("即将发送数据element:"+element.hashCode());
writer.write(element);
writer.flush();
}else {
throw new Exception("socket关闭");
}
} catch (Exception e) {
//将queue.take对象放回去
log.error("server write thread:"+e.getMessage(), e);
try {
//log.info("回收数据element:"+element.hashCode());
queue.put(element);
writer.close();
} catch (IOException | InterruptedException e1) {
writer=null;
log.error("关闭writer异常:"+e1.getMessage(), e1);
}
break;
}
}
}
}
上文中有用到LinkedBlockingQueue这是一个线程安全的阻塞队列,负责数据传输。因为while循环中如果要传数据普通的函数传参是行不通的,所以用到了这个。
接下来是client:
public class MyClientSocket {
private Logger log = Logger.getLogger(MyClientSocket.class);
private int port = 17921;
private String host = "localhost";
private LinkedBlockingQueue<byte[]> queue = new LinkedBlockingQueue<byte[]>(100);
//private LinkedBlockingQueue<JSONObject> sendQueue = new LinkedBlockingQueue<JSONObject>(100);
//下发消息的线程池
private ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) Executors.newFixedThreadPool(20);
private Socket socket = null;
private OutputStream writer;
public LinkedBlockingQueue<byte[]> getQueue() {
return queue;
}
public void setQueue(LinkedBlockingQueue<byte[]> queue) {
this.queue = queue;
}
public void start() {
new Thread(new Runnable() {
@Override
public void run() {
new Thread(new ClientSocket()).start();
}
}).start();
}
public void sendHeartbeat(int code) {
new Thread(new Runnable() {
//初始化赋值心跳write hashcode
private int writeHashCode = code;
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000 * 10 * 2);
//如果socket被关闭就不发送
if(!socket.isClosed() && writeHashCode == writer.hashCode()) {
queue.put(ClientSocketUtil.sendHeart(CommonUtils.getNum()));
}else {
log.error("socket关闭或者输出流不是同一个对象,关闭当前心跳线程");
break;
}
} catch (Exception e) {
log.error("client发送心跳包:"+e.getMessage(), e);
break;
}
}
}
}).start();
}
private class ClientSocket implements Runnable{
@Override
public void run() {
while (true) {
try {
if (socket == null || socket.isClosed()) {
socket = new Socket(host, port);//连接socket
log.info("已连接");
writer=socket.getOutputStream();
if(socket!=null) {
new Thread(new ClientReadThread(socket,queue,threadPoolExecutor)).start();
new Thread(new ClientWriteThread(socket,writer,queue)).start();
sendHeartbeat(writer.hashCode());
}
}
} catch (Exception e) {
if(e instanceof ConnectException) {
continue;
}else {
log.error("client socket:"+e.getMessage(), e);
}
}
}
}
}
}
以上代码中有用到线程池,这个是因为我这边业务需要,因为要群发命令给硬件,不能因为某个命令卡住了导致后面的都阻塞,所以用了线程池。
还有是关于断线重连的逻辑,大家可以看下,特别是心跳线程的处理,用了hashCode判断socket输出流是不是同一个对象,因为前一个socket断掉了,这个心跳线程其实还能跑,所以我是这么做的,如果有更好的做法希望大家能教教我。
reader和writer线程跟server的逻辑几乎一模一样。不贴了,贴了可能会影响观看。
逻辑其实很简单。
如有不足请大家指出,本人不才,献丑了。
浙公网安备 33010602011771号