网络由下往上分为 物理层 、数据链路层 、 网络层 、 传输层 、 会话层 、 表现层 和 应用层。IP协议对应于网络层,TCP协议对应于传输层,而HTTP协议对应于应用层。TCP/IP协议是传输层协议,主要解决数据如何在网络中传输,而HTTP协议是应用层协议,主要解决如何包装数据。
socket,又称套接字,是在不同的进程间进行网络通讯的一种协议、约定或者说是规范。对于socket编程,它更多的时候像是基于TCP/UDP等协议做的一层封装或者说抽象,是一套系统所提供的用于进行网络通信相关编程的接口。
本质上,socket是对tcp连接(当然也有可能是udp等其他连接)协议,在编程层面上的简化和抽象。
BaseSocket:
public class BaseSocket {
public int port;
public String host;
public static final int MAX_BUFFER_SIZE = 1024;
public ServerSocket serverSocket;
public Socket socket;
public InputStream inputStream;
public OutputStream outputStream;
public void close() {
try {
if (this.inputStream != null) {
this.inputStream.close();
}
if (this.outputStream != null) {
this.outputStream.close();
}
if (this.socket != null) {
this.socket.close();
}
if (this.serverSocket != null) {
this.serverSocket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
BaseSocketServer:
public class BaseSocketServer extends BaseSocket {
public int getPort() {
return this.port;
}
public void setPort(int port) {
this.port = port;
}
BaseSocketServer(int port) {
this.port = port;
}
/**
* 单次通信
*/
public void runServerSingle() {
try {
this.serverSocket = new ServerSocket(this.port);
System.out.println("----------base socket server started------------");
this.socket = serverSocket.accept();
this.inputStream = socket.getInputStream();
byte[] readBytes = new byte[MAX_BUFFER_SIZE];
int msgLen;
StringBuilder stringBuilder = new StringBuilder();
while ((msgLen = inputStream.read(readBytes)) != -1) {
stringBuilder.append(new String(readBytes, 0, msgLen, "UTF-8"));
}
System.out.println("Get message from client : " + stringBuilder.toString());
this.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 双向通信
*/
public void runServer() {
try {
this.serverSocket = new ServerSocket(port);
System.out.println("----------base socket server started------------");
this.socket = serverSocket.accept();
this.inputStream = socket.getInputStream();
byte[] readBytes = new byte[MAX_BUFFER_SIZE];
int msgLen;
StringBuilder stringBuilder = new StringBuilder();
while ((msgLen = this.inputStream.read(readBytes)) != -1) {
stringBuilder.append(new String(readBytes, 0, msgLen, "UTF-8"));
}
System.out.println("received message: " + stringBuilder.toString());
//告诉客户端接受完毕,之后只能发送
this.socket.shutdownInput();
this.outputStream = socket.getOutputStream();
String receipt = "we received your message : " + stringBuilder.toString();
this.outputStream.write(receipt.getBytes("UTF-8"));
this.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
BaseSocketServer baseSocketServer = new BaseSocketServer(8888);
//单向通信
//baseSocketServer.runServerSingle();
//双向通信
baseSocketServer.runServer();
}
}
BaseSocketClient:
public class BaseSocketClient extends BaseSocket {
BaseSocketClient(String host, int port) {
this.host = host;
this.port = port;
}
/**
* 获取连接
*/
public void connectServer() {
try {
this.socket = new Socket(this.host, this.port);
this.outputStream = this.socket.getOutputStream();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 单向通信
*
* @param message 消息内容
*/
public void sendSingle(String message) {
try {
this.outputStream.write(message.getBytes("UTF-8"));
this.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 双向通信
* @param message 消息
*/
public void sendMessage(String message) {
try {
this.outputStream.write(message.getBytes("UTF-8"));
//发送完毕
this.socket.shutdownOutput();
this.inputStream = this.socket.getInputStream();
byte[] readBytes = new byte[MAX_BUFFER_SIZE];
int msgLen;
StringBuilder stringBuilder = new StringBuilder();
while ((msgLen = inputStream.read(readBytes)) != -1) {
stringBuilder.append(new String(readBytes, 0, msgLen, "UTF-8"));
}
System.out.println("got receipt: " + stringBuilder.toString());
this.inputStream.close();
this.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
BaseSocketClient baseSocketClient = new BaseSocketClient("127.0.0.1", 8888);
baseSocketClient.connectServer();
//单向通信
//baseSocketClient.sendSingle("hello");
//双向通信
baseSocketClient.sendMessage("hello");
}
}
单向通信详情可以参考,runServerSingle与sendSingle.
单向通信显然有点浪费通道。socket连接支持全双工的双向通信(底层是tcp),上边的例子中,双向通信,服务端在收到客户端的消息后,将返回给客户端一个回执。
双向通信与单向类似,不同的一点是,在 进行一次消息传递之后不是真正意义上的close资源。而是调用了
this.socket.shutdownOutput();
this.socket.shutdownInput();
借此告知服务端或者客户端消息已经发送\接受完毕。调用stream的close会导致sockt的关闭,虽然调用上面两个方法也会关闭流,但不会关闭socket,只是无法继续发送消息。
我们的上述例子在进行一次发送\回执之后就会close,没有办法进行接下来的消息发送。下次需要重新建立连接,这样是很耗费资源的。其实我们可以做到建立一次连接分次发送多条消息,我们有两张方式进行多条消息的区分。
我们先看一个例子:
CycleSocketServer:
public class CycleSocketServer extends BaseSocket{
public int getPort() {
return this.port;
}
public void setPort(int port) {
this.port = port;
}
CycleSocketServer(int port) {
this.port = port;
}
public void runServerForSign() {
try {
this.serverSocket = new ServerSocket(this.port);
System.out.println("base socket server started.");
this.socket = serverSocket.accept();
this.inputStream = socket.getInputStream();
Scanner scanner = new Scanner(inputStream);
//循环接收并打印消息
while (scanner.hasNextLine()) {
System.out.println("get info from client: " + scanner.nextLine());
}
scanner.close();
this.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 根据长度界定,消息传递分为两步
* 1.发送长度
* 2.获取消息
*/
public void runServer() {
try {
this.serverSocket = new ServerSocket(this.port);
this.socket = serverSocket.accept();
System.out.println("server socket running!");
this.inputStream = socket.getInputStream();
byte[] bytes;
while (true) {
//先读取第一个字节
int first = this.inputStream.read();
//是-1则表示输入流已经关闭
if (first == -1) {
this.close();
break;
}
//读取第二个字节
int second = this.inputStream.read();
//用位运算将两个字节拼起来成为真正的长度
int length = (first <<8 ) +second;
bytes = new byte[length];
this.inputStream.read(bytes);
System.out.println("receive message : " + new String(bytes,"UTF-8"));
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
CycleSocketServer cycleSocketServer = new CycleSocketServer(8888);
//cycleSocketServer.runServerForSign();
cycleSocketServer.runServer();
}
}
CycleSocketClient:
public class CycleSocketClient extends BaseSocket {
CycleSocketClient(String host, int port) {
this.host = host;
this.port = port;
}
/**
* 获取连接
*/
public void connectServer() {
try {
this.socket = new Socket(this.host, this.port);
this.outputStream = this.socket.getOutputStream();
} catch (IOException e) {
e.printStackTrace();
}
}
public void sendForSign(String message) {
//约定 \n为消息结束标记
String sendMsg = message + "\n";
try {
this.outputStream.write(sendMsg.getBytes("UTF-8"));
} catch (IOException e) {
System.out.println(sendMsg);
e.printStackTrace();
}
}
public void sendMessage(String message){
try {
//将message转化为bytes数组
byte[] bytes = message.getBytes("UTF-8");
//传输两个字节长度。采用位移实现
int length = bytes.length;
this.outputStream.write(length >> 8);
this.outputStream.write(length);
//传输完长度之后
this.outputStream.write(bytes);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
CycleSocketClient cycleSocketClient = new CycleSocketClient("127.0.0.1", 8888);
cycleSocketClient.connectServer();
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String message = scanner.nextLine();
//cycleSocketClient.send(message);
cycleSocketClient.sendMessage(message);
}
}
}
在上述的例子中,消息的接收方并不能主动地向对方发送消息,换句话说我们并没有实现真正的互相对话,这主要是因为消息的发送和接收这两个动作并不能同时进行,因此我们需要使用两个线程,其中一个用于监听键盘输入并将其写入socket,另一个则负责监听socket并将接受到的消息显示。出于简单考虑,我们直接让主线程负责键盘监听和消息发送,同时另外开启一个线程用于拉取消息并显示。
ListenThread:
public class ListenThread extends BaseSocket implements Runnable {
ListenThread(Socket socket) {
this.socket = socket;
try {
this.inputStream = socket.getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while (true) {
try {
int first = this.inputStream.read();
if (first == -1) {
this.close();
throw new RuntimeException("disconnected");
//break;
}
int second = this.inputStream.read();
int msglen = (first << 8) + second;
byte[] bytes = new byte[msglen];
this.inputStream.read(bytes);
System.out.println("message from [ " + this.socket.getInetAddress() + " ] is " + new String(bytes, "UTF-8"));
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
ChatSocket:
public class ChatSocket extends BaseSocket {
//ExecutorService threadPool = Executors.newFixedThreadPool(100);
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build();
/**
* 使用factory
* 创建线程池
*/
ExecutorService threadPool = new ThreadPoolExecutor(5,10,1,
TimeUnit.SECONDS,new ArrayBlockingQueue<>(1024),threadFactory,new ThreadPoolExecutor.AbortPolicy());
public void runAsServer(int port) {
try {
this.serverSocket = new ServerSocket(port);
System.out.println("server started at port " + port);
//等待客户端的加入
this.socket = this.serverSocket.accept();
System.out.println("successful connected with " + socket.getInetAddress());
//启动监听线程
this.threadPool.submit(new ListenThread(this.socket));
waitAndSend();
} catch (IOException e) {
e.printStackTrace();
}
}
public void runAsClient(String host, int port) {
try {
this.socket = new Socket(host, port);
System.out.println("successful connected to server " + socket.getInetAddress());
this.threadPool.submit(new ListenThread(this.socket));
waitAndSend();
} catch (IOException e) {
e.printStackTrace();
}
}
public void waitAndSend() {
try {
this.outputStream = this.socket.getOutputStream();
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
sendMessage(scanner.nextLine());
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void sendMessage(String message) {
try {
byte[] bytes = message.getBytes("UTF-8");
int length = bytes.length;
this.outputStream.write(length >> 8);
this.outputStream.write(length);
this.outputStream.write(bytes);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
ChatSocket chatSocket = new ChatSocket();
System.out.println("select connect type: 1 for server and 2 for client");
int type = scanner.nextInt();
if (type == 1) {
System.out.print("input server port :");
int port = scanner.nextInt();
chatSocket.runAsServer(port);
} else if (type == 2) {
System.out.print("input server host: ");
String host = scanner.next();
System.out.print("input server port: ");
int port = scanner.nextInt();
chatSocket.runAsClient(host, port);
}
}
}
作为服务端,如果一次只跟一个客户端建立socket连接,未免显得太过浪费资源,因此我们完全可以让服务端和多个客户端建立多个socket。
那么既然要处理多个连接,就不得不面对并发问题了(当然,你也可以写循环轮流处理)。我们可以使用多线程来处理并发,不过线程的创建和销毁都会消耗大量的资源和时间,所以最好一步到位,我们用一个线程池来实现。线程池的相关用法感兴趣的同学可以了解以下。
demo链接