socket网络编程之多线程阻塞IO实例

先介绍一下网络层次结构、socket与TCP/UDP之间的关系。同步、异步,阻塞、非阻塞的区别。

网络由下往上分为物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。

IP 协议对应于网络层,
TCP协议对应于传输层,
HTTP协议对应于应用层,
三者从本质上来说没有可比性,
socket则是对TCP/IP协议的封装和应用。
可以说,TPC/IP协议是传输层协议,主要解决数据如何在网络中传输,而HTTP是应用层协议,主要解决如何包装数据

socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),
通过Socket,我们才能使用TCP/IP协议。
实际上,Socket跟TCP/IP协议没有必然的联系。Socket编程接口在设计的时候,就希望也能适应其他的网络协议。
所以说,Socket的出现只是使得程序员更方便地使用TCP/IP协议栈而已,是对TCP/IP协议的抽象,
从而形成了我们知道的一些最基本的函数接口,比如create、 listen、connect、accept、send、read和write等等

实际上,传输层的TCP是基于网络层的IP协议的,而应用层的HTTP协议又是基于传输层的TCP协议的,
而Socket本身不算是协议,就像上面所说,它只是提供了一个针对TCP或者UDP编程的接口

TCP连接的三次握手:
第一次握手:客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认; 
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。   
握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据
断开连接时服务器和客户端均可以主动发起断开TCP连接的请求,断开过程需要经过“四次握手”

TCP是面向链接的,虽然说网络的不安全不稳定特性决定了多少次握手都不能保证连接的可靠性,
但TCP的三次握手在最低限度上(实际上也很大程度上保证了)保证了连接的可靠性;
而UDP不是面向连接的,UDP传送数据前并不与对方建立连接,对接收到的数据也不发送确认信号,
发送端不知道数据是否会正确接收,当然也不用重发,所以说UDP是无连接的、不可靠的一种数据传输协议

也正由于上面的特点,使得UDP的开销更小数据传输速率更高,因为不必进行收发数据的确认,所以UDP的实时性更好。

同步/异步主要针对C端: 
同步:
      所谓同步,就是在c端发出一个功能调用时,在没有得到结果之前,该调用就不返回。也就是必须一件一件事做,等前一件做完了才能做下一件事。
例如普通B/S模式(同步):提交请求->等待服务器处理->处理完毕返回 这个期间客户端浏览器不能干任何事


异步:
      异步的概念和同步相对。当c端一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。
     例如 ajax请求(异步): 请求通过事件触发->服务器处理(这是浏览器仍然可以作其他事情)->处理完毕


阻塞/非阻塞主要针对S端:


阻塞
     阻塞调用是指调用结果返回之前,当前线程会被挂起(线程进入非可执行状态,在这个状态下,cpu不会给线程分配时间片,即线程暂停运行)。函数只有在得到结果之后才会返回。


     有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。 例如,我们在socket中调用recv函数,如果缓冲区中没有数据,这个函数就会一直等待,直到有数据才返回。而此时,当前线程还会继续处理各种各样的消息。
   快递的例子:比如到你某个时候到A楼一层(假如是内核缓冲区)取快递,但是你不知道快递什么时候过来,你又不能干别的事,只能死等着。但你可以睡觉(进程处于休眠状态),因为你知道快递把货送来时一定会给你打个电话(假定一定能叫醒你)。


非阻塞
      非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。
     还是等快递的例子:如果用忙轮询的方法,每隔5分钟到A楼一层(内核缓冲区)去看快递来了没有。如果没来,立即返回。而快递来了,就放在A楼一层,等你去取。

服务端和客户端之间通过Socket建立连接,之后它们就可以进行通信了。首先ServerSocket将在服务端监听某个端口,当发现客户端有Socket来试图连接它时,它会acceptSocket的连接请求,同时在服务端建立一个对应的Socket与之进行通信。这样就有两个Socket了,客户端和服务端各一个。

下面写一个阻塞IO下客户端与服务端异步通信的例子,每次ServerSocket接收到一个新的Socket连接请求后都会新起一个线程来跟当前Socket进行通信。

服务端代码

public class server {
public static void main(String args[]) throws IOException{
//为了简单起见,所有的异常信息都往外抛  
 int port = 8899;  
     //定义一个ServerSocket监听在端口8899上  
     ServerSocket server = new ServerSocket(port);  


     while(true){
   
     //server尝试接收其他Socket的连接请求,server的accept方法是阻塞式的  
     Socket socket=server.accept();
     
     //每接收到一个Socket就建立一个新的线程来处理它  
     new Thread(new Task(socket)).start();
     }
}
static class Task implements Runnable{
//静态内部类
 
private Socket socket;

public Task(Socket socket){
this.socket=socket;
}


@Override
public void run() {
try{
handleSocket();
}
catch(Exception e){
e.printStackTrace();
}
}

private void handleSocket() throws Exception{
//跟客户端建立好连接之后,我们就可以获取socket的InputStream,并从中读取客户端发过来的信息了。  
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));  
        StringBuilder sb = new StringBuilder();  
        String temp;  
        int index;  
        while ((temp=br.readLine()) != null) {  
        //使用BufferedReader来一次读一行
           if ((index = temp.indexOf("eof")) != -1) {//遇到eof时就结束接收  
            sb.append(temp.substring(0, index));  
               break;  
           }  
           sb.append(temp);  
        } 
        //socket.setSoTimeout(20*1000);
        System.out.println("from client: " + sb);  
        //读完后写一句  
        Writer writer = new OutputStreamWriter(socket.getOutputStream());  
        writer.write("Hello Client.");  
        writer.write("eof\n");  
        writer.flush();  
        writer.close();  
        br.close();  
        socket.close();  
}


}


}


客户端代码

public class client {
private static List<Queue> queueCache = new LinkedList<Queue>();
public static void main(String args[]) throws Exception {  
 
     //为了简单起见,所有的异常都直接往外抛  
     String host = "192.168.1.55";  //要连接的服务端IP地址  
     int port = 8899;   //要连接的服务端对应的监听端口  
     //与服务端建立连接  
     Socket client = new Socket(host, port);  
     //建立连接后就可以往服务端写数据了  
     Writer writer = new OutputStreamWriter(client.getOutputStream());
     writer.write("Hello Server.");  
     writer.write("eof\n"); 
     writer.flush();//写完后要记得flush
     BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream()));  
     StringBuffer sb = new StringBuffer();  
     String temp;  
     int index;  
     while ((temp=br.readLine()) != null) {  
        if ((index = temp.indexOf("eof")) != -1) {  
           sb.append(temp.substring(0, index));  
           break;  
        }  
        sb.append(temp);  
     }  
     System.out.println("from server: " + sb);  


     br.close();
     writer.close();  
     client.close();  
  }  
}



BufferedReaderreadLine方法是一次读一行的,这个方法是阻塞的,直到它读到了一行数据为止程序才会继续往下执行,那么readLine什么时候才会读到一行呢?直到程序遇到了换行符或者是对应流的结束符readLine方法才会认为读到了一行,才会结束其阻塞,让程序继续往下执行。所以我们在使用BufferedReaderreadLine读取数据的时候一定要记得在对应的输出流里面一定要写入换行符(流结束之后会自动标记为结束,readLine可以识别),写入换行符之后一定记得如果输出流不是马上关闭的情况下记得flush一下,这样数据才会真正的从缓冲区里面写入。

Socket为我们提供了一个setSoTimeout()方法来设置接收数据的超时时间,单位是毫秒。当设置的超时时间大于0,并且超过了这一时间Socket还没有接收到返回的数据的话,Socket就会抛出一个SocketTimeoutException

你可能感兴趣的:(多线程,socket,网络编程)