新学期生活开始一段时间了,要继续学习一些新的技术(这里指socket /doge),目标是尝试完成一个在线即时聊天的小程序(尽量不咕)。会更新一系列socket编程的技术文章,欢迎关注交流~
那么千里之行,始于足下,就从这socket编程开始说起吧。
socket概念
首先一个问题,什么是socket编程?他有个中文名称叫做“套接字编程”。这个词不直观,也比较晦涩,很容易让人产生误解。我们来看一下百度百科的定义:
简介:socket一般指套接字。所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。
一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制
大致可以明白其含义是在网络上,两台主机的进程实现通信的编程技术。
但是描述还是有些不够形象,那么其实从英文原意的角度来看,socket的翻译有“插座”的意思
这么看,将两个要相互通信的主机比作是插座和插头,发起的一方是插头,接受的一方是插座,二者的ip和端口对接上之后即可相互传输信息。这个socket可以说是上接相应的应用程序,下接通信协议栈,保证信息快速完整的传递。
这个比喻虽然不够恰当,但是也足够形象的体现socket编程的含义。
确定一个socket的标识有两个,分别是:IP和端口
表示方法是点分十进制的IP地址和端口号,中间使用冒号隔开,例如:127.0.0.1:8888
就是电脑本机的8888
端口
Socket的工作流程
一次socket的连接与通信大致可分为以下的步骤:
- 服务端:启动程序并开始监听连接
- 客户端:启动程序并请求连接
- 客户端:建立连接后像服务端发送信息
- 服务端:建立连接并接受客户端发送的信息
- 服务端:完成信息接受后向客户端回复信息
- 客户端:完成信息传输后接收服务端的回复信息
- 客户端:会话完毕,关闭socket
- 服务端:会话完毕,关闭socket并选择关闭ServerSocket或者继续监听
当然,我们也可以选择在服务端采用多线程的形式来完成多个客户端请求的情况,从而避免服务端被一个客户端霸占,后序客户端排队的情况。这样的工作流程在刚刚的基础上可以被表示为:
- 服务端:启动程序并开始监听连接
- 客户端:启动程序并请求连接
- 客户端:建立连接后像服务端发送信息
- 服务端:确认连接并开启一个线程用来处理通信
- 服务端:建立连接并接受客户端发送的信息
- 服务端:完成信息接受后向客户端回复信息
- 客户端:完成信息传输后接收服务端的回复信息
- 客户端:会话完毕,关闭socket
- 服务端:会话完毕,关闭socket并选择关闭ServerSocket或者继续监听
了解了大致的工作流程,下面就来了解一下socket的连接和通信
Socket连接
那么使用Java如何实现socket编程呢?
其实就是需要分别实现客户端(Client)和服务端(Server)的socket,即服务端开放“插座”等待匹配,客户端使用“插头”匹配服务端的插头。
最核心需要用到两个类,他们都在java.net
包中:
- Socket类,位于客户端(client)使用的“插头”,初始化时需要指定连接对象的IP和端口。
- ServerSocket类,服务端(Server)的“插座”,初始化时需要制定开放的端口(服务器本机IP)
服务端需要使用ServerSocket中一个重要的方法来获取客户端的连接:accept()
这个方法可以获得客户端的socket对象,服务端使用这个对象中的io流与客户端进行通信,同样的,客户端使用socket中的io流与服务端进行通信。
上面这段描述需要注意的有两点:
- 客户端与服务端通过socket对象来通信,客户端创建socket,服务端使用ServerSocket对象接收并获取socket
- 客户端与服务端必须通过socket对象提供的io流进行通信,流中的内容会通过网络相互传输。
下面就来简单的实现一个客户端与服务端的对接:
//客户端
public class SocketClient {
public static void main(String[] args) throws UnknownHostException, IOException {
System.out.println("客户端程序~~~");
System.out.println("我创建了一个socket");
Socket client = new Socket("127.0.0.1",8888);
}
}
//服务端
public class SocketServer {
public static void main(String[] args) throws IOException {
System.out.println("服务端程序~~~");
System.out.println("我创建了一个ServerSocket");
ServerSocket server = new ServerSocket(8888);
System.out.println("开始接受socket匹配");
Socket client = server.accept();
System.out.println("接收到了一个socket");
}
}
此时,运行服务端:
发现程序并没有执行后面的语句输出提示。这个原因在于:accept()
方法在等待连接时会使程序产生阻塞,不在往下执行,直到接受到一个连接,并且返回一个客户端的Socket对象实例。
那么接着就运行客户端,让他们相互匹配:
此时,服务端就接收到了一个socket连接,执行了后续的语句。
可以看出,在客户端,连接是在创建socket对象时就发起的,并不需要调用任何方法。
在服务端,需要使用accept方法来监听连接 ,当没有socket连接时,程序就会阻塞。
Socket通信
在建立了连接之后,服务端和客户端就要开始通信了。
上文提到过,两方的通信是通过字节流来完成的,而且这个字节流必须是socket对象提供的io流。
这个io流需要通过socket的getInputStream
和getOutputStream
两个方法来获取。
在使用io流进行通信的过程中,有一个需要注意的地方,就是服务端的输出流对接的是客户端的输出流,而客户端的输入流对接的是服务端的输出流。
用一张图来表示:
其实也很符合直觉的,但是在编写程序过程中,尤其是同时编写客户端和服务端的程序时,这两个流的方向是容易搞混的,需要注意一下。
单向传递消息
现在就先从客户端向服务端传递一条消息:
/*客户端*/
public class SocketClient {
public static void main(String[] args) throws UnknownHostException, IOException {
System.out.println("客户端程序~~~");
System.out.println("我创建了一个socket");
Socket client = new Socket("127.0.0.1",8888);
System.out.println("正在行服务端发送消息");
/*向服务端发送消息*/
client.getOutputStream().write("你好服务端,这里是客户端".getBytes());
System.out.println("像服务端发送消息完毕");
/*会话结束*/
client.close();
}
}
/*服务端*/
public