在客户端/服务器通信模式中,Socket是双方通信通道的抽象封装,用户可通过配置Socket的参数并构建Socket来完成双方的连接,并通过此通道进行网络通信。
1、构造Socket
Socket的构造方法主要有以下几种重载形式:
- Socket()
- Socket(InetAddress address, int port) throws UnknowHostException, IOException
- Socket(InetAddress address, int port, InetAddress localAddr, int localPort) throws IOException
- Socket(String host, int port) throws UnknowHostException, IOException
- Socket(String host, int port, InetAddress localAddr, int localPort) throws IOException
当客户端Socket构造方法与服务器建立连接时,需要等待一段时间,默认会一直等待下去,直到连接成功,或出现异常。受底层网络传输速度等影响,连接可能处于长时间等待状态,可设置连接超时时间进行限定。
1.1、无参构造方法
Socket()
使用无参构造方法,创建一个无连接的Socket,同时使用系统默认的Socket实现;
Socket socket = new Socket();
SocketAddress remoteAddr = new InetSocketAddress("localhost",8000);
socket.connect(remoteAddr, 60000); //连接服务器并设置连接超时时间为1分钟
1.2、设定服务器的地址
- Socket(InetAddress address, int port) //参数address 表示主机的IP地址
- Socket(String host, int port) //参数host 表示主机的名字,当host为null时,连接本地回环地址;
InetAddress 类表示服务器的IP地址, InetAddress 类提供了一系列静态工厂方法, 用于构造自身的实例:
- 返回本地主机的IP地址
InetAddress addr1 = InetAddress.getLocalHost();
- 返回代表 "222.34.5.7"的 IP地址
InetAddress addr2 = InetAddress.getByName("222.34.5.7");
- 返回域名为"www.javathinker.org"的 IP地址
InetAddress addr3 = InetAddress.getByName("www.javathinker.org");
1.3、设定客户端地址
- Socket(InetAddress address, int port, InetAddress localAddr, int localPort)throws IOException
- Socket(String host, int port, InetAddress localAddr, int localPort) throws IOException
一个Socket中既包含服务器地址也包含客户端地址,默认情况下,客户端地址为客户端所在主机的ip,端口为系统随机分配的空闲端口。以上两种构造方法支持应用显式设置客户端ip和端口;
2、客户端连接服务器的异常类
当Socket的构造方法连接服务器时,可能抛出如下异常:
- UnknownHostException:如果无法识别主机的名字或IP地址,就会抛出此种异常。
- ConnectException:如果没有服务器进程监听指定的端口,或者服务器进程拒绝连接,就会抛出这种异常。
- SocketTimeoutException:如果等待连接超时,就会抛出这种异常。
- BindException:如果无法把Socket对象与指定的本地IP地址或端口绑定,就会抛出这种异常。
类继承关系图:
|---- IOException------- UnknownHostException
|---- InterruptedIOException ----------- SocketTimeoutException
|---- SocketException ----------- BindException
|---------- ConnectException
3、获取Socket信息
以下方法用于获取Socket的有关信息:
- getInetAddress():获得连接的远程服务器的IP地址。
- getRemoteSocketAddress():获取连接的远程服务器地址;
- getLocalSocketAddress():获取本地绑定的地址;
- getPort():获得远程服务器的端口。
- getLocalAddress():获得客户本地的IP地址。
- getLocalPort():获得客户本地的端口。
- getInputStream():获得输入流。如果Socket还没有连接,或者已经关闭,或者已经通过shutdownInput()方法关闭输入流,那么此方法会抛出IOException。
- getOutputStream():获得输出流。如果Socket还没有连接,或者已经关闭,或者已经通过shutdownOutput()方法关闭输出流,那么此方法会抛出IOException。
4、关闭Socket
4.1 、Socket.close()
当客户与服务器的通信结束,应该及时关闭Socket,以释放Socket占用的包括端口在内的各种资源。当一个Socket对象被关闭,就不能再通过它的输入输出流进行I/O操作,否则会导致IOException。Socket的close()方法负责关闭Socket。推荐代码如下:
Socket socket=null;
try{
socket=new Socket("www.javathinker.org",80);
//执行接收和发送数据的操作
…
}catch(IOException e){
e.printStackTrace();
}finally{
try{
if(socket!=null)socket.close();
}catch(IOException e){
e.printStackTrace();
}
}
4.2、Socket状态
- isClosed():如果Socket已经连接到远程主机,并且还没有关闭,则返回true,否则返回false;
- isConnected():如果Socket曾经连接到远程主机,则返回true,否则返回false。
- isBound():如果Socket已经与一个本地端口绑定,则返回true,否则返回false。
如果要判断一个Socket对象当前是否处于连接状态:
boolean isConnected=socket.isConnected() && !socket.isClosed();
5、半关闭Socket
有的时候,可能仅仅希望关闭输出流或输入流之一。此时可以采用Socket类提供的半关闭方法:
- shutdownInput():关闭输入流。
- shutdownOutput():关闭输出流。
Socket类还提供了两个状态测试方法,用来判断输入流和输出流是否关闭:
- public boolean isInputShutdown()
- public boolean isOutputShutdown()
6、Socket选项
Socket有以下选项:
6.1、TCP_NODELAY
设置该选项:public void setTcpNoDelay(boolean on) throws SocketException //底层实现不支持,则抛出异常。
读取该选项:public boolean getTcpNoDelay() throws SocketException
默认情况下,发送数据采用Negale算法。Negale算法是指发送方发送的数据不会立刻发出,而是先放在缓冲区内,等待缓冲区满了在发出。 发送完一批数据后,会等待接收方对这批数据的回应,然后在发送下一批数据。(适用于发送方需要发送大批量数据,并且接收方会及时作出回应的场合,而如果持续发送小批量数据,且接收方不一定会看立即发送响应数据,则使发送方会运行很慢。)
TCP_NODELAY的默认值为false,表示使用Negale算法, 若 setTcpNoDelay(true)方法,就会关闭Socket的缓冲,让数据及时发送。
if( !socket.getTcpNoDelay() ){ socket.setTcpNoDelay(true); }
6.2、SO_REUSEADDR
设置该选项:public void setResuseAddress(boolean on) throws SocektException
读取该选项:public boolean getResuseAddress() throws SocketException
当接收方通过Socket的colse()方法关闭Socket时,如果网络上还有发送到这个Socket的数据,那么底层的Socket不会立刻释放而本地端口,而是会等待一段时间,确保接收到了网络上发送过来的延迟数据,然后再释放端口。但接收到延迟数据后,不会对这些数据做任何处理,只是确保这些数据不会被其他碰巧绑定到同样端口的新进程接收到。
而当服务器程序关闭后,有可能它的端口还会被占用一段时间,如果此时立刻在同一个主机上重启服务器程序,由于端口已经被占用,使得服务器程序无法绑定到该端口,启动失败。
为使同一个主机上的其他进程还可以立刻重用该端口,可以调用Socket的setReuseAddress(true);
if( !socket.getReuseAddress() ){ socket.setReuseAddress(true); }
但值得注意的是 socket.setReuseAddress(true)方法必须在Socket还没有绑定到一个本地端口之前调用,否则执行该方法就无效。因此必须按下方式创建Socket对象,在连接远程服务器。
Socket socket = new Socket(); //此时Socket对象未绑定本地端口,并且未连接远程服务器 socket.setReuseAddress(true); // 会抛出SocketException
// 如果不写以下这句话,则客户程序一般采用随机端口,那么救护绑定匿名的本地端口。 SocketAddress localAddr = new InetSocketAddress("localhost",9999); SocketAddress remoteAddr = new InetSocketAddress("remotehost",8888);
socket.bind(localAddr); //与本地端口绑定
socket.connect(remoteAddr); // 连接远程服务器
//两个共用同一个端口的进程必须都调用该方法才能使得一个进程关闭Socket后,另一个进程的Socket能够立刻重用相同的端口。
6.3、SO_TIMEOUT
设置该选项:public void setSoTimeout(int timeout) throws SocketException
读取该选项:public int getSoTimeOut() throws SocketException
当通过Socket的输入流读数据时,如果还没有数据,就会等待。例如:
byte[] buff = new byte[1024]; InputStream in = socket.getInputStream(); in.read(buff);
对于上述代码,如果输入流中没有数据,in.read(buff)就会等待对方发送数据,知道满足以下情况才结束等待:
输入流中有1024个字节,read()方法吧这些字节读入到buff中,在返回读取的字节数。
当寂静块接近输入流的末尾,距离末尾还有小于1024个字节时,read()方法会把这些字节读入到buff中,在返回读取的字节数。
已经读到输入流的末尾,正常结束,返回-1.
连接已经断开,抛出IOException。
如果通过Socket的setSoTimeout()方法设置了等待超时时间,单位为毫秒,超过该时间后就抛出SocketTimeoutException。 它的默认值为0,表示会无限等待,永远不会超时。
public class ReceiveServer {
public static void main(String[] args) throws IOException,SocketException{
ServerSocket server = new ServerSocket(9999);
Socket s = server.accept(); s.setSoTimeout(1000*20); //设置超时时间为5秒
InputStream in = s.getInputStream();
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
byte[] buff = new byte[1024];
int length = -1;
do{
try {
length = in.read(buff);
if( length != -1 ){
buffer.write( buff,0,length );
}
} catch (SocketTimeoutException e) {
//e.printStackTrace();
System.out.println("等待超时...");
}
}while( length != -1 );
System.out.println( new String( buffer.toByteArray()) ); // 将字节数组转换为字符串
}
}
public class SendClient {
public static void main(String[] args) throws Exception {
Socket socket = new Socket("localhost",9999);
OutputStream out = socket.getOutputStream();
out.write( "Hello ".getBytes() );
out.write("EveryOne".getBytes() );
Thread.sleep(1000*60); socket.close();
}
}
6.4、SO_LINGER
设置该选项:public void setSoLinger(boolean on,int seconds) throws SocketException // 注意第二个参数以秒为单位,并非毫秒
读取该选项:public int getSoLinger() throws SocketException
SO_LINGER选项用来控制Socket关闭时的行为,默认情况下,当执行Socket的close()方法,该方法会立即返回,但底层的Socket实际上并不立即关闭,它会延迟一段时间知道发送完所有剩余的数据,才会真正关闭Socket,才断开连接。
但对于以下情况:
socket.setSoLinger(true,0);
//该方法也会立即返回,但底层的Socket也会立即关闭,所有未发送完的剩余数据被丢弃。 //还在网络上传输,但是未被接收方接收的数据, socket.setSoLinger(true,1000); // 而该方法不会立即返回,而是进入阻塞状态。 只有当底层的Soket发送完所有的剩余数据或阻塞时间已经超过了1000秒,也回返回,但是剩余未发送的数据被丢弃。
//对于该选项,尝试着让服务器端先睡眠一会,再开始接受数据:
服务器端程序:
public class SimpleServer {
public static void main(String[] args) throws Exception {
ServerSocket server = new ServerSocket(7777);
Socket socket = server.accept();
System.out.println("服务器困了,于是决定休息会...");
Thread.sleep( 1000*10 ); // 睡眠10秒。
System.out.println("服务器终于睡醒了,然后开始接受数据:");
InputStream in = socket.getInputStream();
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
byte[] buff = new byte[1024];
int length = -1;
while( ( length = in.read(buff) ) != -1){
buffer.write(buff,0,length);
}
System.out.println( new String( buffer.toByteArray() )); //把字节数组转换成字符串。
}
} //客户端程序:
public class SimpleClient {
public static void main(String[] args) throws Exception {
Socket socket = new Socket("localhost",7777); //socket.setSoLinger(true, 0);
//(1) 该条件下,会强行关闭底层的Socket,导致所有未发送数据丢失,而服务器算接受方睡醒后接受数据,由于底层Socket关闭,则会抛出SocketException: Connection reset. //socket.setSoLinger(true, 300);
//(2) 注意Socket关闭时间,实际上会等待一段时间在关闭
OutputStream out = socket.getOutputStream();
StringBuffer sb = new StringBuffer();
String str = "关于对setSoLinger选项的设置:";
sb.append( str );
for( int i=1;i<=10000;i++){
// 需注意jvm堆栈的设置。
sb.append( i ); }
System.out.println( sb );
out.write( sb.toString().getBytes() );
System.out.println("开始关闭Socket: ");
long start = System.currentTimeMillis();
socket.close(); // 注意不同条件下(1)与(2)的关闭的时间
long end = System.currentTimeMillis();
System.out.println("关闭Socket所用时间为: " + (end - start ) +"ms" );
}
}
6.5.SO_RCVBUF选项:
该选项用于 输入数据 的缓冲区的大小。
设置该选项:public void setReceiveBufferSize(int size) throws SocketException //
读取该选项:public int getReceiveBufferSize() throws SocketException
6.6.SO_SNDBUF选项:
该选项用于 输出数据的缓冲区的大小。
设置该选项:* public void setSendBufferSize(int size) throws SocketException; //
读取该选项: public void getSendBufferSize() throws SocketException;
public class SocketBufferDemo {
public static void main(String[] args) throws SocketException {
Socket socket = new Socket();//不带参的构造方法,不会试图建立与服务器端的连接.
//默认情况下的输入输出缓冲区的大小:
int rcvbuf = socket.getReceiveBufferSize(); // 输入缓冲区大小
int sndbuf = socket.getSendBufferSize(); //输出缓冲区大小
System.out.println( rcvbuf + "\t" + sndbuf ); //重新设置输入输出缓冲区的大小再输出结果:
socket.setReceiveBufferSize( 1024*32 );
socket.setSendBufferSize( 1024*32 );
System.out.println( socket.getReceiveBufferSize()+"\t"+socket.getSendBufferSize() );
}
}
6.7.SO_KEEPALIVE选项:
设置该选项: public void setKeepAlive(boolean on) throws SocketException //
读取该选项: public int getKeepAlive() throws SocketException
当该选项为true时,表示底层的TCP实现会监视该连接是否有效。当连接处于空闲状态(即连接的两端没有互相传送数据)超过2H,本地的TCP实现会发送一个数据包给远程的Socket。如果远程Socket没有发回响应,TCP实现就会持续尝试11分钟,直到接收到响应为止。如果在12分钟内未收到响应,TCP实现就会自动关闭本地Socket,断开连接。(在不同的网络平台上,TCP实现尝试与远程Socket对话的时限会有所差别)。
该选项默认值为 false,即表示TCP不会监视连接是否有效,不活动的客户端可能会永久存在下去,而不会注意到服务器已经崩溃。
对于KeepAlive这种系统底层的机制(用于系统维护每一个tcp连接的),可在认识到另一种新的概念,即心跳线程:
链接网址:
[http://code.taobao.org/p/tfs/wiki/dataserver/background/] <心跳线程简单介绍>
[http://blog.csdn.net/xuyuefei1988/article/details/8279812] <关于心跳包机制>
[http://blog.sina.com.cn/s/blog_616e189f0100s3px.html] <这篇也不错,Socket缓冲区探讨>
6.8.OOBINLINE选项:
设置该选项: public void setOOBInline(boolean on ) throws SocketException //
读取该选项: public void getOOBInline() throws SocketException public void sendUrgentData(int data) throws IOException //虽然sendUrgentData的参数data是int类型,但只有这个int类型的低字节被发送(即一个字节,8位),其它的三个字节被忽略。
该选项为true时,表示支持发送一个字节的TCP紧急数据。Socket类的sendUrgentData( int data ) 方法用于发送一个字节(8位)的TCP紧急数据。
而为false时,表示当接收方收到紧急数据时不作任何处理,直接丢弃。
但是问题是接收方会把接收到的紧急数据与普通数据放在同样的队列中,除非使用一些更高层次的协议,否则接收方处理紧急数据的能力非常有限,当紧急数据到来时,接收方不会得到任何的通知,因此接收方很难区分普通数据与紧急数据,只好按同样的方式处理他们。
服务端代码:
public class OOBInlineServer {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(7777);
System.out.println("服务器已经启动,端口号:7777");
while (true){
Socket socket = serverSocket.accept(); // 须在服务端中也设置为true,否则无法接受到客户端发送过来的紧急数据:
socket.setOOBInline(true);
InputStream in = socket.getInputStream();
InputStreamReader inReader = new InputStreamReader(in);
BufferedReader bReader = new BufferedReader(inReader);
System.out.println(bReader.readLine());
System.out.println(bReader.readLine());
socket.close();
}
}
} //
**客户端代码:**
public class OOBInlineClient {
public static void main(String[] args) throws Exception {
Socket socket = new Socket("localhost", 7777);
socket.setOOBInline(true);
OutputStream out = socket.getOutputStream();
OutputStreamWriter outWriter = new OutputStreamWriter(out);
outWriter.write(67); // 向服务器发送字符"C"
outWriter.write(" Hello World\r\n ");
socket.sendUrgentData(65); // "A"
socket.sendUrgentData(68); // "D"
outWriter.flush();
socket.sendUrgentData(322); // "B" 322分布在两个字节上,但是其低位为:0100 0010 即刚好跟 66 一样,
socket.close();
}
}
猜测下上述输出结果: 猜测可能为:
C Hello World
A D B
但正常结果输出为:
[图片上传失败...(image-22cb57-1538206377829)]
由此可见:使用sendUrgentData()方法发送数据后,系统会立即将这些数据发送出去,而使用write()(首先是将数据存放在了缓冲区)发送数据,必须要使用flush()方法才会真正发送数据。
另外注意的是:在使用 setOOBInline()方法时,要注意必须在客户端和服务端程序同时使用该方法,打开SO_OOBInline选项,否则无法命名sendUrgentData来发送数据。
相关阅读:
ServerSocket详解 【https://www.jianshu.com/p/665994c2e784】
参考博客:
https://my.oschina.net/gently/blog/531117
https://blog.csdn.net/swartz_lubel/article/details/79574472
https://blog.csdn.net/LastDays_L/article/details/48004547
http://elf8848.iteye.com/blog/1961194
参考书籍:
孙卫琴 《java网络编程精解》
代码示例:
https://github.com/zhaozhou11/java-io.git
(com.zhaozhou.demo.socket包)