前言
Socket套接字在TCP/IP中扮演着非常重要的角色。
1:为什么
为了搞清楚为什么socket对tpcip协议意义非凡,它是怎么工作的,到底是怎么发挥作用。
2:是什么
socket是BSD UNIX的通信机制,通常称为“套接字”,其英文原意是“孔”或“插座”。有些顾名思义,socket正如其英文原意一样,像是一个多孔插座,可以提供多个端口的连接服务。 Socket其实就是一个门面模式,它把复杂的TCP/IP(里面内容很多,包含TCP协议、UDP协议)协议族隐藏在Socket接口后面(程序员的视角:socket是对TPC/IP协议的封装,用起来更方便),对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。 Socket本身不算是协议,它只是提供了一个针对TCP或者UDP编程的接口。
- 像多孔插座
如果说socket是一个多孔插座,插座是提供各种电器供电的地方,不同的电器工作时需要的电压和电流也不一样,但各种电器都有各自的一个插口,这个称之为“端口”。
电器使用的电可以看做是网络资源或者是各种“流”,电是由电线传输过来的,所以插座需要连接电线,这里电线也就是服务器和客户端连接 “connection”。
其中初始化socket的过程像是买来一个插座的安装过程。
在插座这边的是“客户端”,电线那边提供电的发电厂是“服务器”。
客户端和发电厂都各自拥有一个地址,即“IP地址”。
其中还有一套传输和用电的规则,比如传输电时需要的电线多少伏才能满足需求,电器用电的技术参数,端口是几个孔的。这个是“协议”。
正常情况下我们是不会去管协议的内容是什么,也就是说协议在我们面前是隐藏的。
- 工作原理
对于服务器来说,服务器先初始化socket,然后端口绑定(bind),再对端口监听(listen),调用accept阻塞,等待客户端连接请求。对于客户端来说,客户端初始化socket,然后申请连接(connection)。客户端申请连接,服务器接受申请并且回复申请许可(这里要涉及TCP三次握手连接),然后发送数据,最后关闭连接,这是一次交互过程
- Socket和NioSocket
Java中的Socket分为普通的Socket和NioSocket。
普通的Socket:
ServerSocket用于服务器端,可以通过accept方法监听请求,监听请求后返回Socket,Socket用于完成具体数据传输,客户端也可以使用Socket发起请求并传输数据。ServerSocket的使用可以分为三步:
1:创建ServerSocket。ServerSocket的构造方法有5个,其中最方便的是ServerSocket(int port),只需要一个port就可以了。
2:调用创建出来的ServerSocket的accept方法进行监听。accept方法是阻塞方法,也就是说调用accept方法后程序会停下来等待连接请求,在接受请求之前程序将不会继续执行,当接收到请求后accept方法返回一个Socket。
3:使用accept方法返回的Socket与客户端进行通信
NioSocket:
从JDK1.4开始,Java增加了新的IO模式-nio(new IO),nio在底层采用了新的处理方式,极大提高了IO的效率。我们使用的Socket也是IO的一种,nio提供了相应的工具:ServerSocketChannel和SocketChannel,他们分别对应原来的ServerSocket和Socket。
服务器端NioSocket的处理过程:
1:创建ServerSocketChannel并设置相应的端口号、是否为阻塞模式 2:创建Selector并注册到ServerSocketChannel上 3:调用Selector的selector方法等待请求 4:Selector接收到请求后使用selectdKeys返回SelectionKey集合 5:使用SelectionKey获取到channel、selector和操作类型并进行具体操作。
3:怎么用
普通的Socket
public class SocketConstants {
//主机地址
public static final String HOST_ADDR = "127.0.0.1";
//端口号
public static final int PORT = 10003;
}
- 服务端:
public class TcpServer { public static void main(String[] args) throws Exception { //建立服务端socket服务,并监听一个port ServerSocket ss = new ServerSocket(PORT); //通过accept方法获取连接过来的client对象 Socket s = ss.accept(); String ip = s.getInetAddress().getHostAddress(); //获取client发送过来的数据。使用client对象的读取流来读取数据 InputStream in = s.getInputStream(); byte[] buf = new byte[1024]; int len = in.read(buf); System.out.println(new String(buf,0,len)); //关闭client s.close(); } }
- 客户端:
public class TcpClient { public static void main(String[] args) throws Exception { //创建client的socket服务,指定目的主机和port Socket s = new Socket(HOST_ADDR,PORT); //为了发送数据。获取socket流中的输出流 java.io.OutputStream out = s.getOutputStream(); out.write("hello tcp".getBytes()); s.close(); } }
NioSocket:
public class NIOServer {
public static void main(String[] args) {
// TODO Auto-generated method stub
try {
//创建ServerSocketChannel,监听8080端口
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress(8080));
//设置为非阻塞模式
ssc.configureBlocking(false);
//为ssc注册选择器
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
//创建处理器
Handler handler = new Handler(1024);
while(true){
//等待请求,每次等待阻塞3s,超过3s后线程继续向下运行,如果传入0或者不传入参数则一直阻塞
if(selector.select(3000) == 0){
System.out.println("等待请求超时----");
continue;
}
System.out.println("处理请求----");
//获取处理的SelectionKey
Iterator<SelectionKey> keyIter = selector.selectedKeys().iterator();
while(keyIter.hasNext()){
SelectionKey key = keyIter.next();
try{
//接收到连接请求时
if(key.isAcceptable()){
handler.handleAccept(key);
}
//读数据
if(key.isReadable()){
handler.handleRead(key);
}
}catch(IOException ex){
keyIter.remove();
continue;
}
//处理完后,从待处理的SelectionKey迭代器中移除当前所使用的key
keyIter.remove();
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private static class Handler{
private int bufferSize = 1024;
private String localCharset = "UTF-8";
public Handler(int bufferSize){
this.bufferSize = bufferSize;
}
public void handleAccept(SelectionKey key) throws IOException{
SocketChannel sc = ((ServerSocketChannel) key.channel()).accept();
sc.configureBlocking(false);
sc.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(bufferSize));
}
public void handleRead(SelectionKey key) throws IOException{
//获取Channel
SocketChannel sc = (SocketChannel) key.channel();
//获取buffer并重置
ByteBuffer buffer = (ByteBuffer)key.attachment();
buffer.clear();
//没有读到内容则关闭
if(sc.read(buffer) == -1)
sc.close();
else{
//将buffer转换为读状态
buffer.flip();
//将buffer中接收到的值按localCharset格式编码后保存到receivedString
String receivedString = Charset.forName(localCharset).newDecoder().decode(buffer).toString();
System.out.println("received from client:" + receivedString);
//返回数据给客户端
String sendString = "this data is from Server";
buffer = ByteBuffer.wrap(sendString.getBytes(localCharset));
sc.write(buffer);
sc.close();
}
}
}
}
//客户端代码通普通Socket一样,Socket socket = new Socket("192.168.6.42",8080)
4:跟http的区别
- 区别1:socket是接口,致力于传输层的套接字;http是应用层协议(可能区别本身应该描述成:传输层与应用层的区别更合适)
- 区别2:socket是长连接(有些场景需要配合高速轮训机制保持连接处于活跃性);http是短连接
http 为短连接:客户端发送请求都需要服务器端回送响应.请求结束后,主动释放链接,因此为短连接。通常的做法是,不需要任何数据,也要保持每隔一段时间向服务器发送”保持连接”的请求。这样可以保证客户端在服务器端是”上线”状态。 HTTP连接使用的是”请求-响应”方式,不仅在请求时建立连接,而且客户端向服务器端请求后,服务器才返回数据。
Socket为长连接:通常情况下Socket 连接就是 TCP 连接,因此 Socket 连接一旦建立,通讯双方开始互发数据内容,直到双方断开连接。在实际应用中,由于网络节点过多,在传输过程中,会被节点断开连接,因此要通过轮询高速网络,该节点处于活跃状态。
5:跟TCP和UDP的关系
socket编程有两种通信协议可以进行选择。一种是数据报通信,另一种就是流通信 。(前者就是UDP通信,后者是TCP通信。)
UDP是一种无连接的协议,这就意味着我们每次发送数据报时,需要同时发送本机的socket描述符和接收端的socket描述符。
因此,我们在每次通信时都需要发送额外的数据。
UDP用来实现实时性较高或者丢包不重要的一些服务。在局域网中UDP的丢包率都相对比较低。比如语音、视频聊天等。
TCP是一种基于连接的协议。在使用流通信之前,我们必须在通信的一对儿socket之间建立连接。其中一个socket作为服务器进行监听连接请求。另一个则作为客户端进行连接请求。一旦两个socket建立好了连接,他们可以单向或双向进行数据传输。
TCP适合于诸如远程登录(rlogin,telnet)和文件传输(FTP)这类的网络服务。
Socket选择可以指定Socket类发送和接受数据的方式。在JDK1.4中共有8个Socket选择可以设置。这8个选项都定义在java.net.SocketOptions接口中。定义如下:
public final static int TCP_NODELAY = 0x0001;
public final static int SO_REUSEADDR = 0x04;
public final static int SO_LINGER = 0x0080;
public final static int SO_TIMEOUT = 0x1006;
public final static int SO_SNDBUF = 0x1001;
public final static int SO_RCVBUF = 0x1002;
public final static int SO_KEEPALIVE = 0x0008;
public final static int SO_OOBINLINE = 0x1003;
有趣的是,这8个选项除了第一个没在SO前缀外,其他7个选项都以SO作为前缀。
其实这个SO就是Socket Option的缩写;因此,在Java中约定所有以SO为前缀的常量都表示Socket选项;
当然,也有例外,如TCP_NODELAY.在Socket类中为每一个选项提供了一对get和set方法,分别用来获得和设置这些选项。
引用: