TCP(Transmission Control Protocol 传输控制协议)是一种面向链接的、可靠的、基于字节流的传输层通讯协议,由IETF的RFC 793定义。在简化的计算机网络OSI模型中,它完成第四层传输层所指定的功能,用户数据报协议(UDP,下一篇博客会实现)是同一层内 另外一个重要的传输协议。在因特网协议族(Internet protocol suite)中,TCP层是位于IP层之上,应用层之下的中间层。不一样主机的应用层之间常常须要可靠的、像管道同样的链接,可是IP层不提供这样的流机制,而是提供不可靠的包交换。html
应用层向TCP层发送用于网间传输的、用8位字节表示的数据流,而后TCP把数据流分区成适当长度的报文段(一般受该计算机链接的网络的数据链路层的最大传输单元( MTU)的限制)。以后TCP把结果包传给IP层,由它来经过网络将包传送给接收端实体的TCP层。TCP为了保证不发生丢包,就给每一个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。而后接收端实体对已成功收到的包发回一个相应的确认(ACK);若是发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据包就被假设为已丢失将会被进行重传。TCP用一个校验和函数来检验数据是否有错误;在发送和接收时都要计算校验和。java
所谓socket 一般也称做”套接字“,用于描述IP地址和端口,是一个通讯链的句柄。应用程序一般经过”套接字”向网络发出请求或者应答网络请求。web
以J2SDK-1.3为例,Socket和ServerSocket类库位于java.net包中。ServerSocket用于服务器端,Socket是创建网络链接时使用的。在链接成功时,应用程序两端都会产生一个Socket实例,操做这个实例,完成所需的会话。对于一个网络链接来讲,套接字是平等的,并无差异,不由于在服务器端或在客户端而产生不一样级别。不论是Socket仍是ServerSocket它们的工做都是通过SocketImpl类及其子类完成的。编程
重要的Socket API:服务器
java.net.Socket继承于java.lang.Object,有八个构造器,其方法并很少,下面介绍使用最频繁的三个方法,其它方法你们能够见JDK-1.3文档。网络
. Accept方法用于产生”阻塞”,直到接受到一个链接,而且返回一个客户端的Socket对象实例。”阻塞”是一个术语,它使程序运行暂时”停留”在这个地方,直到一个会话产生,而后程序继续;一般”阻塞”是由循环产生的。多线程
. getInputStream方法得到网络链接输入,同时返回一个InputStream对象实例。
. getOutputStream方法链接的另外一端将获得输入,同时返回一个OutputStream对象实例。socket
注意:其中getInputStream和getOutputStream方法均会产生一个IOException,它必须被捕获,由于它们返回的流对象,一般都会被另外一个流对象使用。tcp
既然不论是Socket仍是ServerSocket它们的工做都是通**过SocketImpl类及其子类完成的,那么固然要介绍啦。函数
抽象类 SocketImpl 是实际实现套接字的全部类的通用超类。建立客户端和服务器套接字均可以使用它。
具体JDK见:
http://www.javaweb.cc/help/JavaAPI1.6/index.html?java/nio/ReadOnlyBufferException.html
因为它是超类具体代码实现仍是见下面的Socket
具体API见:http://www.javaweb.cc/help/JavaAPI1.6/index.html?java/nio/ReadOnlyBufferException.html
构造方法:
ServerSocket() ~建立非绑定服务器套接字。
ServerSocket(int port) ~建立绑定到特定端口的服务器套接字。
ServerSocket(int port, int backlog) ~利用指定的 backlog 建立服务器套接字并将其绑定到指定的本地端口号。
ServerSocket(int port, int backlog, InetAddress bindAddr) ~使用指定的端口、侦听 backlog 和要绑定到的本地 IP 地址建立服务器。
除了第一个不带参数的构造方法之外, 其余构造方法都会使服务器与特定端口绑定, 该端口有参数 port 指定. 例如, 如下代码建立了一个与 80 端口绑定的服务器:
ServerSocket serverSocket = new ServerSocket(80);
若是运行时没法绑定到 80 端口, 以上代码会抛出 IOException, 更确切地说, 是抛出 BindException, 它是 IOException 的子类. BindException 通常是由如下缘由形成的:
若是把参数 port 设为 0, 表示由操做系统来为服务器分配一个任意可用的端口. 有操做系统分配的端口也称为匿名端口. 对于多数服务器, 会使用明确的端口, 而不会使用匿名端口, 由于客户程序须要事先知道服务器的端口, 才能方便地访问服务器.
当服务器进程运行时, 可能会同时监听到多个客户的链接请求. 例如, 每当一个客户进程执行如下代码:
Socket socket = new Socket("www.javathinker.org", 80);
就意味着在远程 www.javathinker.org 主机的 80 端口上, 监听到了一个客户的链接请求. 管理客户链接请求的任务是由操做系统来完成的. 操做系统把这些链接请求存储在一个先进先出的队列中. 许多操做系统限定了队列的最大长度, 通常为 50 . 当队列中的链接请求达到了队列的最大容量时, 服务器进程所在的主机会拒绝新的链接请求. 只有当服务器进程经过 ServerSocket 的 accept() 方法从队列中取出链接请求, 使队列腾出空位时, 队列才能继续加入新的链接请求.
对于客户进程, 若是它发出的链接请求被加入到服务器的请求链接队列中, 就意味着客户与服务器的链接创建成功, 客户进程从 Socket 构造方法中正常返回. 若是客户进程发出的链接请求被服务器拒绝, Socket 构造方法就会抛出 ConnectionException.
Tips: 建立绑定端口的服务器进程后, 当客户进程的 Socket构造方法返回成功, 表示客户进程的链接请求被加入到服务器进程的请求链接队列中. 虽然客户端成功返回 Socket对象, 可是还没跟服务器进程造成一条通讯线路. 必须在服务器进程经过 ServerSocket 的 accept() 方法从请求链接队列中取出链接请求, 并返回一个Socket 对象后, 服务器进程这个Socket 对象才与客户端的 Socket 对象造成一条通讯线路.
ServerSocket 构造方法的 backlog 参数用来显式设置链接请求队列的长度, 它将覆盖操做系统限定的队列的最大长度. 值得注意的是, 在如下几种状况中, 仍然会采用操做系统限定的队列的最大长度:
在ServerSocket 构造方法中没有设置 backlog 参数.
如下的 Client.java 和 Server.java 用来演示服务器的链接请求队列的特性.
Client.java
1 import java.net.Socket; 2 public class Client { 3 public static void main(String[] args) throws Exception{ 4 final int length = 100; 5 String host = "localhost"; 6 int port = 1122; 7 Socket[] socket = new Socket[length]; 8 for(int i = 0;i<length;i++){ 9 socket[i] = new Socket(host,port); 10 System.out.println("第"+(i+1)+"次链接成功!"); 11 } 12 Thread.sleep(3000); 13 for(int i=0;i<length;i++){ 14 socket[i].close(); 15 } 16 } 17 }
Server.java
1 import java.io.IOException; 2 import java.net.ServerSocket; 3 import java.net.Socket; 4 public class Server { 5 private int port = 1122; 6 private ServerSocket serverSocket; 7 8 public Server() throws Exception{ 9 serverSocket = new ServerSocket(port,3); 10 System.out.println("服务器启动!"); 11 } 12 public void service(){ 13 while(true){ 14 Socket socket = null; 15 try { 16 socket = serverSocket.accept(); 17 System.out.println("New connection accepted "+ 18 socket.getInetAddress()+":"+socket.getPort()); 19 } catch (IOException e) { 20 e.printStackTrace(); 21 }finally{ 22 if(socket!=null){ 23 try { 24 socket.close(); 25 } catch (IOException e) { 26 e.printStackTrace(); 27 } 28 } 29 } 30 } 31 } 32 33 public static void main(String[] args) throws Exception{ 34 Server server = new Server(); 35 Thread.sleep(60000*10); 36 server.service(); 37 } 38 }
⑴ 在Server 中只建立一个 ServerSocket 对象, 在构造方法中指定监听的端口为1122 和 链接请求队列的长度为 3 . 构造 Server 对象后, Server 程序睡眠 10 分钟, 而且在 Server 中不执行 serverSocket.accept() 方法. 这意味着队列中的链接请求永远不会被取出. 运行Server 程序和 Client 程序后, Client程序的打印结果以下:
第 1 次链接成功
第 2 次链接成功
第 3 次链接成功
Exception in thread “main” java.net.ConnectException: Connection refused: connect
…………….
从以上打印的结果能够看出, Client 与 Server 在成功地创建了3 个链接后, 就没法再建立其他的链接了, 由于服务器的队已经满了.
⑵ 在Server中构造一个跟 ⑴ 相同的 ServerSocket对象, Server程序不睡眠, 在一个 while 循环中不断执行 serverSocket.accept()方法, 该方法从队列中取出链接请求, 使得队列能及时腾出空位, 以容纳新的链接请求. Client 程序的打印结果以下:
第 1 次链接成功
第 2 次链接成功
第 3 次链接成功
………..
第 100 次链接成功
从以上打印结果能够看出, 此时 Client 能顺利与 Server 创建 100 次链接.(每次while的循环要够快才行, 若是太慢, 从队列取链接请求的速度比放链接请求的速度慢的话, 不必定都能成功链接)
若是主机只有一个IP 地址, 那么默认状况下, 服务器程序就与该IP 地址绑定. ServerSocket 的第 4 个构造方法 ServerSocket(int port, int backlog, InetAddress bingAddr) 有一个 bindAddr 参数, 它显式指定服务器要绑定的IP 地址, 该构造方法适用于具备多个IP 地址的主机. 假定一个主机有两个网卡, 一个网卡用于链接到 Internet, IP为 222.67.5.94, 还有一个网卡用于链接到本地局域网, IP 地址为 192.168.3.4. 若是服务器仅仅被本地局域网中的客户访问, 那么能够按以下方式建立 ServerSocket:
ServerSocket serverSocket = new ServerSocket(8000, 10, InetAddress.getByName(“192.168.3.4”));
ServerSocket 有一个不带参数的默认构造方法. 经过该方法建立的 ServerSocket 不与任何端口绑定, 接下来还须要经过 bind() 方法与特定端口绑定.
这个默认构造方法的用途是, 容许服务器在绑定到特定端口以前, 先设置ServerSocket 的一些选项. 由于一旦服务器与特定端口绑定, 有些选项就不能再改变了.好比:SO_REUSEADDR 选项
在如下代码中, 先把 ServerSocket 的 SO_REUSEADDR 选项设为 true, 而后再把它与 8000 端口绑定:
ServerSocket serverSocket = new ServerSocket(); serverSocket.setReuseAddress(true); //设置 ServerSocket 的选项 serverSocket.bind(new InetSocketAddress(8000)); //与8000端口绑定
若是把以上程序代码改成:
ServerSocket serverSocket = new ServerSocket(8000); serverSocket.setReuseAddress(true);//设置 ServerSocket 的选项
那么 serverSocket.setReuseAddress(true) 方法就不起任何做用了, 由于 SO_REUSEADDR 选项必须在服务器绑定端口以前设置才有效.
客户端:
1 import java.io.BufferedReader; 2 import java.io.IOException; 3 import java.io.InputStream; 4 import java.io.InputStreamReader; 5 import java.io.OutputStream; 6 import java.io.PrintWriter; 7 import java.net.Socket; 8 import java.net.UnknownHostException; 9 10 /* 11 * 客户端 12 */ 13 public class Client { 14 public static void main(String[] args) { 15 try { 16 //1.建立客户端Socket,指定服务器地址和端口 17 Socket socket=new Socket("localhost", 8888); 18 //2.获取输出流,向服务器端发送信息 19 OutputStream os=socket.getOutputStream();//字节输出流 20 PrintWriter pw=new PrintWriter(os);//将输出流包装为打印流 21 pw.write("用户名:whf;密码:789"); 22 pw.flush(); 23 socket.shutdownOutput();//关闭输出流 24 //3.获取输入流,并读取服务器端的响应信息 25 InputStream is=socket.getInputStream(); 26 BufferedReader br=new BufferedReader(new InputStreamReader(is)); 27 String info=null; 28 while((info=br.readLine())!=null){ 29 System.out.println("我是客户端,服务器说:"+info); 30 } 31 //4.关闭资源 32 br.close(); 33 is.close(); 34 pw.close(); 35 os.close(); 36 socket.close(); 37 } catch (UnknownHostException e) { 38 e.printStackTrace(); 39 } catch (IOException e) { 40 e.printStackTrace(); 41 } 42 } 43 }
服务器:
1 import java.io.BufferedReader; 2 import java.io.IOException; 3 import java.io.InputStream; 4 import java.io.InputStreamReader; 5 import java.io.OutputStream; 6 import java.io.PrintWriter; 7 import java.net.InetAddress; 8 import java.net.ServerSocket; 9 import java.net.Socket; 10 11 /* 12 * 基于TCP协议的Socket通讯,实现用户登录 13 * 服务器端 14 */ 15 public class Server { 16 public static void main(String[] args) { 17 try { 18 //1.建立一个服务器端Socket,即ServerSocket,指定绑定的端口,并监听此端口 19 ServerSocket serverSocket=new ServerSocket(8888); 20 Socket socket=null; 21 //记录客户端的数量 22 int count=0; 23 System.out.println("***服务器即将启动,等待客户端的链接***"); 24 //循环监听等待客户端的链接 25 while(true){ 26 //调用accept()方法开始监听,等待客户端的链接 27 socket=serverSocket.accept(); 28 //建立一个新的线程 29 ServerThread serverThread=new ServerThread(socket); 30 //启动线程 31 serverThread.start(); 32 33 count++;//统计客户端的数量 34 System.out.println("客户端的数量:"+count); 35 InetAddress address=socket.getInetAddress(); 36 System.out.println("当前客户端的IP:"+address.getHostAddress()); 37 } 38 } catch (IOException e) { 39 e.printStackTrace(); 40 } 41 } 42 }
服务器处理类:
1 import java.io.BufferedReader; 2 import java.io.IOException; 3 import java.io.InputStream; 4 import java.io.InputStreamReader; 5 import java.io.OutputStream; 6 import java.io.PrintWriter; 7 import java.net.Socket; 8 9 /* 10 * 服务器线程处理类 11 */ 12 public class ServerThread extends Thread { 13 // 和本线程相关的Socket 14 Socket socket = null; 15 16 public ServerThread(Socket socket) { 17 this.socket = socket; 18 } 19 20 //线程执行的操做,响应客户端的请求 21 public void run(){ 22 InputStream is=null; 23 InputStreamReader isr=null; 24 BufferedReader br=null; 25 OutputStream os=null; 26 PrintWriter pw=null; 27 try { 28 //获取输入流,并读取客户端信息 29 is = socket.getInputStream(); 30 isr = new InputStreamReader(is); 31 br = new BufferedReader(isr); 32 String info=null; 33 while((info=br.readLine())!=null){//循环读取客户端的信息 34 System.out.println("我是服务器,客户端说:"+info); 35 } 36 socket.shutdownInput();//关闭输入流 37 //获取输出流,响应客户端的请求 38 os = socket.getOutputStream(); 39 pw = new PrintWriter(os); 40 pw.write("欢迎您!"); 41 pw.flush();//调用flush()方法将缓冲输出 42 } catch (IOException e) { 43 // TODO Auto-generated catch block 44 e.printStackTrace(); 45 }finally{ 46 //关闭资源 47 try { 48 if(pw!=null) 49 pw.close(); 50 if(os!=null) 51 os.close(); 52 if(br!=null) 53 br.close(); 54 if(isr!=null) 55 isr.close(); 56 if(is!=null) 57 is.close(); 58 if(socket!=null) 59 socket.close(); 60 } catch (IOException e) { 61 e.printStackTrace(); 62 } 63 } 64 } 65 }