网络程序设计 第二章 客户端Socket用法详解
概述 在客户/服务器通信模式中,客户端需要主动创建与服务器连接的Socket(套接字),服务器段收到客户端的连接请求,也会创建与客户连接的Socket。 Socket可以看作是通信连接两端的收发器,服务端和客户端都是通过Socket来收发数据。 注意:这里的Socket是指客户端应用编程的Socket类,而服务器端的是ServerSocket.
本章的内容 1、Socket类介绍,构造方法/成员方法 2、Socket对象选项设置,控制建立与服务器端的连接,以及收发数据的行为。 3、介绍一个SMTP客户程序,它利用Socket连接到SMTP邮件发送服务器,然后请求服务器发送一封邮件。
2.1 构造Socket Socket的构造方法: 1、Socket() 2、Socket(InetAddress address,int port) 3、Socket(InetAddress address,int port, InetAddress localAddr,int localport) 4、 Socket(String host,int port) 5、 Socket(String host,int port, InetAddress localAddr,int localport)
2.1 InetAddress 在这里InetAddress 表示服务器的IP地址。 InetAddress addr = InetAddress.getLocalHost(); //返回代表“172.17.7.250”的IP地址 InetAddress addr = InetAddress.getByName(“172.17.7.250”); //返回域名为www.bnuep.com 的IP地址: 3. InetAddress addr = InetAddress.getByName(“www.bnuep.com”);
2.1 设置服务器地址 在这里除了第一个不带参数的构造方法,其他构造方法都需要在参数中设定服务器的IP地址或主机名,以及端口:第2和第4构造函数
2.1 设置客户端地址 在一个Socket对象中既包含远程服务器的IP地址和端口信息,也包含本机客户端的IP地址和端口信息。默认情况下,客户端IP地址来自于客户程序所在的主机,而其端口则由操作系统随机分配。 Socket类有两个构造方法允许显式设定客户端的IP地址或主机名和端口:3,5
2.1 使用无参构造函数 当客户端Socket构造方法请求与服务器连接时,可能需要等待一段时间,如果希望等待连接的时间,我们使用第一个无参数的构造函数,如下: Socket socket = new Socket(); SocketAddress remoteAddr = new InetSocketAddress(“localhost”,8000); Socket.connect(remoteAddr,60000); 其中connect(SocketAddress endpoint,int timeout)方法负责连接服务器。
2.1 客户连接服务器时可能抛出的异常 public void connect(String host,int port){ SocketAddress remoteAddr=new InetSocketAddress(host,port); Socket socket=null; String result=""; try { long begin=System.currentTimeMillis(); socket = new Socket(); socket.connect(remoteAddr,1000); //超时时间为1秒钟 long end=System.currentTimeMillis(); result=(end-begin)+"ms"; //计算连接所花的时间 }
2.1 客户连接服务器时可能抛出的异常 catch (BindException e) { result="Local address and port can't be binded"; }catch (UnknownHostException e) { result="Unknown Host"; }catch (ConnectException e) {//如果ServerSocket对象设定连接请求队列长度 result="Connection Refused"; }catch (SocketTimeoutException e) { result="TimeOut"; }catch (IOException e) { result="failure"; } finally { try { if(socket!=null)socket.close(); } catch (IOException e) { e.printStackTrace(); } } System.out.println(remoteAddr+" : "+result); }
2.2获取Socket的信息 1、前面提到了,一个Socket对象同时包含了服务器端和客户端的IP地址和端口信息。 如何编程呢?
2.2获取Socket的信息 getInetAddress(); getPort(); getLocalAddress(); getLocalPort(); getInputStream();可以使用shutdownInput()关闭输入流 getOutputStream();可以使用shutdownOutput()关闭输出流
2.2HTTPClient展示输入输出流 public class HTTPClient { String host="localhost"; int port=6888; Socket socket; public void createSocket()throws Exception{ socket=new Socket("localhost",6888); }
2.2HTTPClient展示输入输出流 public void communicate()throws Exception{ StringBuffer sb=new StringBuffer("GET "+"/crm/About.jsp"+" HTTP/1.1\r\n"); sb.append("Host: www.zh-hr.com\r\n"); sb.append("Accept: */*\r\n"); sb.append("Accept-Language: zh-cn\r\n"); sb.append("Accept-Encoding: gzip, deflate\r\n"); sb.append("User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)\r\n"); sb.append("Connection: Keep-Alive\r\n\r\n"); //发出HTTP请求 OutputStream socketOut=socket.getOutputStream(); socketOut.write(sb.toString().getBytes()); socket.shutdownOutput(); //关闭输出流
2.2HTTPClient展示输入输出流 //接收响应结果 InputStream socketIn=socket.getInputStream(); BufferedReader br = new BufferedReader(new InputStreamReader(socketIn)); String Data ; while ((Data=br.readLine())!=null) System.out.println(Data); socket.close(); }
2.2HTTPClient展示输入输出流 getInetAddress(); getPort(); getLocalAddress(); getLocalPort(); getInputStream(); getOutputStream();
2.3关闭Socket 当客户端与服务端通信结束,应该及时关闭Socket,以释放Socket占用的包括端口在内的各种资源。 Socket的close()方法负责关闭Socket,当Socket对象被关闭,就不能对其输入输出流进行操作,否则会导致IOException 参考前面编程的方式关闭Socket
2.3测试Socket的连接状态 Socket类提供3个状态测试方法: 1、isClosed():如果Socket已经连接到远程主机,但调用了close(),则返回true,否则为false. 2、isConnected():如果Socket曾经连接到远程主机,则返回true,否则false. 3、isBound():如果Socket已经与一个本地端口绑定,则返回true,否则false. 4、如要判断一个Socket对象当前是否处于连接状态,可以如下: Boolean isConnected = socket.isConnected() && !socket.isClosed();
2.4半关闭Socket 进程A与进程B通过Socket通信,假定进程A输出数据,进程B读入数据,进程A如何告知进程B所有数据输出完毕了呢? 有以下四种方法:
2.4半关闭Socket 1、进程A与进程B交换的是字符流,并且都一行一行的读/写数据时,可以事先约定以一个特殊的标志为结束标志,如第一章例子。 2、进程A先发送消息告知进程B所发送的正文的长度,然后再发送正文,进程B只读进相应长度的字符或字节即可。 3、进程A发完所有数据后,关闭Socket。这时进程B读完这些数据后,再执行输入流的read()方法时,该方法返回-1。如果使用BufferdReader的readLine(),则返回null。如下:
2.4半关闭Socket ByteArrayOutputStream buffer=new ByteArrayOutputStream(); byte[] buff=new byte[1024]; int len=-1; while((len=socketIn.read(buff))!=-1){ buffer.write(buff,0,len); }
2.4半关闭Socket BufferedReader br = new BufferedReader(new InputStreamReader(socketIn)); String Data ; while ((Data=br.readLine())!=null) System.out.println(Data);
2.4半关闭Socket 4、当调用Socket的close()方法关闭Socket时,它的输入输出流也都会被关闭。但有时可能仅希望关闭其中之一,可以采用Socket类提供的半关闭方法: shutdownInput():关闭输入流。 shutdownOutput():关闭输出流。 案例HTTPClient采用了半关闭方式
2.4半关闭Socket Socket类提供了输入输出流的状态测试方法: isInputShutDown() isOutputShutDown() 测试:当客户与服务器通信时:如果有一方突然结束程序,或者关闭了Socket,或者单独关闭了输入流或输出流,对另一方会造成什么影响呢?请通过书中实验来理解。
2.5设置Socket选项 对Socket网络通信行为的控制可以通过通过以下选项进行设置: 1、TCP_NODELAY:表示立即发送数据。 2、SO_REUSEADDR:表示是否允许重用Socket所绑定的本地地址。 3、SO_TIMEOUT:表示接收数据时的等待超时时间 4、SO_LINGER:表示当执行Socket的close()方法时,是否立即关闭底层的Socket。 5、SO_SNDBUF:表示发送数据的缓冲区的大小
2.5设置Socket选项 对Socket网络通信行为的控制可以通过通过以下选项进行设置: 6、SO_RCVBUF:表示接收数据的缓冲区的大小。 7、SO_KEEPALIVE:表示对于长时间处于空闲状态的Socket,是否要自动把它关闭。 8、OOBINLINE:表示是否支持发送一个字节的TCP紧急数据。
2.5设置Socket选项 对Socket网络通信行为的控制可以通过以下选项进行设置: 1、TCP_NODELAY:表示立即发送数据。 设置方法:setTcpNoDelay(boolean on) 读取方法:boolean getTcpNoDelay() false适用情形:大批量数据发送,如文件数据 True适用情形:实时传输,如网络游戏
2.5设置Socket选项 2、SO_REUSEADDR:表示是否允许重用Socket所绑定的本地地址。 设置方法:setReuseAddress(boolean on) 读取方法:boolean getReuseAddress() 为了确保一个进程关闭Socket后,即使它还没有释放端口(需要确保接收完延迟数据,确保这些数据不被其他绑定到该端口的新进程接收到),同一主机的其他进程还可以立即重用该端口,可以在Socket没有绑定本地端口调用之前调用setReuseAddress(true),然后才调用connect(remoteAddr)。
2.5设置Socket选项 3、SO_TIMEOUT:表示接收数据时的等待超时时间 设置方法:setSoTimeout(int milliseconds) 读取方法:int getSoTimeout() 在Socket接收数据前调用setSoTimeout(1000),用于设定接收数据的等待超时时间,单位为毫秒,其默认值为0,表示永不超时。 注意实验时,我们设置客户端的SO_TIMEOUT,而不是服务器端的(书中为服务器端)。
2.5设置Socket选项 4、SO_LINGER:表示当执行Socket的close()方法时,是否立即关闭底层的Socket。默认会延迟直到发送完所有剩余的数据,才会真正关闭Socket,断开连接。 设置方法:setSoLinger(boolean on,int seconds) 读取方法:int getSoLinger() 执行socket.setSoLinger(true,0):底层Socket立即关闭,剩余数据丢弃。 执行socket.setSoLinger(true,3600):底层Socket已经发送完剩余数据,或者阻塞超过3600秒,才关闭和丢弃剩余数据。
2.5设置Socket选项 6、SO_RCVBUF:表示接收数据的缓冲区的大小。 一般说来,传输大的连续的数据块(基于http/ftp协议通信)可以使用较大缓冲区,这样减少数据传输的次数,提高传输效率 而对于交互频繁且单次传输数据量比较小的通信方式(telnet/网络游戏),则应采取较小的缓冲区,确保小批量的数据能及时发送给对方。
2.5设置Socket选项 5、SO_SNDBUF:表示发送数据的缓冲区的大小 设置方法:setSendBufferSize(int size) 读取方法:int getSendBufferSize () Receive的设置: 设置方法:setReceiveBufferSize(int size) 读取方法:int getReceiveBufferSize () 类似SO_RCVBUF
2.5设置Socket选项 7、SO_KEEPALIVE:表示对于长时间处于空闲状态的Socket,是否要自动把它关闭。 当此项为True时,表示底层的TCP实现会监视该连接是否有效,当连接处于空闲状态超过了两小时,本地的tcp实现会发送一个数据包给远程的socket。如果远程socket没有发回响应,tcp实现就会持续尝试11分钟,直到收到响应为止。如果未收到响应,tcp实现就会自动关闭本地Socket,断开连接。 在不同网络平台,时限可能不同。 而当其为false时,表示Tcp实现不会监视连接是否有效,不活动的客户段可能会永久存在,而不会注意到服务器已经崩溃
2.5设置Socket选项 8、OOBINLINE:表示是否支持发送一个字节的TCP紧急数据。其默认值为false,这时接收方对紧急数据不做任何处理,直接丢弃。 该项为true时,表示支持发送一个字节的TCP紧急数据,socket.setOOBInline(true)。 Socket类的sendUrgentData(int data)方法用于发送一个字节的TCP紧急数据。
2.5设置Socket选项 9、服务类型选项:在internet上传输数据分为不同的服务类型,它们有不同的定价,类比邮局提供的不同服务—普通信、挂号信、快件,区分速度和可靠性。 用户可以选择不同服务类型:例如发送视频,需要高带宽快速送达目的地,发送电子邮件可以使用低带宽慢速到达目的地。
2.5设置Socket选项 9、服务类型选项: IP规定4种服务类型,用来定性的描述服务的质量。 1、低成本:发送成本低。 2、高可靠性:保证把数据可靠的送达目的地。 3、最高吞吐量:一次可以接收或发送大批量的数据。 4、最小延迟:传输数据的速度快,把数据快速送达目的地。
2.5设置Socket选项 9、服务类型选项:这4种服务类型还可以进行组合。例如:可以同时要求获得高可靠性和最小延迟。Socket类中对应的方法如下: 设置方法:setTrafficClass(int trafficClass) 读取方法:int getTrafficClass() Socket类用4个16进制整数表示服务类型。 低成本:0x02(0000,0010) 高可靠性:0x04(0000,0100) 最高吞吐量:0x08(0000,1000) 最小延迟:0x10(0001,0000)
2.5设置Socket选项 9、服务类型选项:这4种服务类型还可以进行组合。例如:可以同时要求获得高可靠性和最小延迟。对应的设置方法如下: Socket.setTrafficClass(0x04|0x10) 高可靠性设置: Socket.setTrafficClass(0x04)
2.5设置Socket选项 10、设定连接时间、延迟和带宽的相对重要性:在jdk1.5中,socket类提供了一个方法: setPerformancePreferences(int connectionTime,int latency,int bandwidth) 上述方法的3个参数表示网络传输数据的3项指标:最小时间建立连接、最小延迟、最高带宽。
2.5设置Socket选项 10、设定连接时间、延迟和带宽的相对重要性: 在Internet上,数据传输之前先被分割成包。当数据包到达目的地,接收方再以正确的次序把数据包重新装配起来。所谓延迟就是一个数据包到达目的地需要的时间;另外,当你发出对某个目标的请求,目标系统在应答时返回一些数据包,延迟这一概念也可用来描述这一过程所需要的时间。最理想的组合当然是高带宽、低延迟:数据包能够以最快的速度到达,且有充裕的带宽支持;反之,低带宽、高延迟则属于最差的组合。
2.5设置Socket选项 10、设定连接时间、延迟和带宽的相对重要性: 许多通过卫星建立的连接会出现高带宽、高延迟的情形,例如,虽然带宽高达768Kbps,如果你点击网页上的链接或发送一个命令,收到应答数据可能需要一秒以上。由于延迟时间太长,对于许多联机游戏来说,基于卫星的连接几乎无法使用,因为联机游戏要求延迟时间小于半秒或更少。当然,也有的应用对延迟时间的要求不高,例如email。 网络延迟与许多因素有关,最重要的是发送方和接收方之间的路由器,它们对连接的质量有着重要的影响。卫星连接之所以比光缆连接慢,就是因为数据必须通过轨道上的卫星中转。Cable和DSL线路的延迟一般要小得多,但最终还是要由线路的具体情况决定。
2.6发送邮件的SMTP客户程序 STMP协议:建立在TCP/IP协议之上。 规定了把邮件从发送方传输到接收方的规则 STMP客户程序请求发送邮件,STMP服务器负责把邮件传输到目的地。
2.6发送邮件的SMTP客户程序 SMTP设计基于以下通信模型:针对用户的邮件请求,发送SMTP建立与接收SMTP之间建立一个双向传送通道。接收SMTP可以是最终接收者也可以是中间传送者。SMTP命令由发送SMTP发出,由接收SMTP接收,而应答则反方面传送。
2.6发送邮件的SMTP客户程序 一旦传送通道建立,SMTP发送者发送MAIL命令指明邮件发送者。如果SMTP接收者可以接收邮件则返回OK应答。SMTP发送者再发出RCPT命令确认邮件是否接收到。如果SMTP接收者接收,则返回OK应答;如果不能接收到,则发出拒绝接收应答(但不中止整个邮件操作),双方将如此重复多次。当接收者收到全部邮件后会接收到特别的序列,如果接收者成功处理了邮件,则返回OK应答。
2.6发送邮件的SMTP客户程序
2.6发送邮件的SMTP客户程序 class Message{ //表示邮件 String from; //发送者的邮件地址 String to; //接收者的邮件地址 String subject; //邮件标题 String content; //邮件正文 String data; //邮件内容,包括邮件标题和正文 public Message(String from,String to, String subject, String content){ this.from=from; this.to=to; this.subject=subject; this.content=content; data="Subject:"+subject+"\r\n"+content; }
2.6发送邮件的SMTP客户程序 import java.net.*; import java.io.*; public class MailSender{ private String smtpServer="smtp.mydomain.com"; //SMTP邮件服务器的主机名 //private String smtpServer="localhost"; private int port=25; public static void main(String[] args){ Message msg=new Message( "tom@abc.com", //发送者的邮件地址 "linda@def.com", //接收者的邮件地址 "hello", //邮件标题 "hi,I miss you very much."); //邮件正文 new MailSender().sendMail(msg); }
2.6发送邮件的SMTP客户程序 public void sendMail(Message msg){ Socket socket=null; try{ socket = new Socket(smtpServer,port); //连接到邮件服务器 BufferedReader br =getReader(socket); PrintWriter pw = getWriter(socket); String localhost= InetAddress.getLocalHost().getHostName(); //客户主机的名字 sendAndReceive(null,br,pw); //仅仅是为了接收服务器的响应数据 sendAndReceive("HELO " + localhost,br,pw); sendAndReceive("MAIL FROM: <" + msg.from+">",br,pw); sendAndReceive("RCPT TO: <" + msg.to+">",br,pw); sendAndReceive("DATA",br,pw); //接下来开始发送邮件内容 pw.println(msg.data); //发送邮件内容 System.out.println("Client>"+msg.data); sendAndReceive(".",br,pw); //邮件发送完毕 sendAndReceive("QUIT",br,pw); //结束通信 }catch (IOException e){ e.printStackTrace(); }finally{ try{if(socket!=null)socket.close(); }catch (IOException e) {e.printStackTrace();} } }
2.6发送邮件的SMTP客户程序 /** 发送一行字符串,并接收一行服务器的响应数据*/ private void sendAndReceive(String str,BufferedReader br,PrintWriter pw) throws IOException{ if (str != null){ System.out.println("Client>"+str); pw.println(str); //发送完str字符串后,还会发送“\r\n”。 } String response; if ((response = br.readLine()) != null) System.out.println("Server>"+response); private PrintWriter getWriter(Socket socket)throws IOException{ OutputStream socketOut = socket.getOutputStream(); return new PrintWriter(socketOut,true); private BufferedReader getReader(Socket socket)throws IOException{ InputStream socketIn = socket.getInputStream(); return new BufferedReader(new InputStreamReader(socketIn));
2.7Socket小结 1、Socket建立在TCP/IP协议基础上,客户端与服务器都通过Socket来收发数据,也就是说两者都需要创建Socket对象来输入和输出流。 2、构造函数 3、正确关闭Socket 4、Socket属性选项设置,用来控制建立连接、接收和发送数据,以及关闭Socket的行为。