首页 > 代码库 > 网络2

网络2

Socket通信

关闭输入输出流的同时,并不关闭网络连接,这就需要用到Socket类的另外两个方法:shutdownInput和shutdownOutput

Android的C文件中定义socket服务并使用

以下是使用android_get_control_socket的方式(/system/core/include/cutils/sockets.h),也可以使用linux的socket_local_server方式定义socket服务端。然后客户端都使用socket_local_client获取socket

1,定义

Init.rc中定义服务名,以及服务对应的执行脚本文件

service <name> <pathname> [ <argument> ]*

<option>

<option>

例如:

1

 service ppp /system/bin/pppd call gprs
     user root
     group system radio
     disabled
     oneshot

2

service mtpd/system/bin/mtpd
    socket mtpd stream 600 system system
    user vpn
    group vpn net_admin net_raw
    disabled
    oneshot

socket <name><type><perm> <user> <group> 

创建一个名字为/dev/socket/<name>的unixdomain socket,并把它的fd传递给 加载的进程。<type>的值是dgram或stream.,

2,服务端代码

编写socket服务端代码,生成可执行脚本htfsk

  1.     //这一步很关键,就是获取init.rc中配置的名为 "htfsk" 的socket  
  2.     fdListen = android_get_control_socket(SOCKET_NAME);  
  3.     //开始监听  
  4.     ret = listen(fdListen, connect_number);    
  5.     //等待Socket客户端发启连接请求  
  6.     new_fd = accept(fdListen, (struct sockaddr *) &peeraddr, &socklen);  
  7.     while(1){  
  8. 10.     //循环等待Socket客户端发来消息  
  9.     }  
  10. //发送消息回执给Socket客户端  
  11.     if(send(new_fd,buff,strlen(buff),0)==-1)  
  12.     {   
  1.     } 

编译成可执行脚本

LOCAL_MODULE:= xxx
LOCAL_SRC_FILES:=xxx.c
include $(BUILD_EXECUTABLE)

3,客户端代码

fd = socket_local_client

通信流程

Socket发送byte,在此之上再有协议(解析这些byte的规则)。HttpURLConnection底层也是用socket。

Unix/linux上叫BSD Socket

可以在本地IPC通信中使用Socket

 技术分享

 

发送:

应用程序调用系统调用,将数据发送给socket
socket检查数据类型,调用相应的send函数
send函数检查socket状态、协议类型,传给传输层
tcp/udp(传输层协议)为这些数据创建数据结构,加入协议头部,比如端口号、检验和,传给下层(网络层)
ip(网络层协议)添加ip头,比如ip地址、检验和
如果数据包大小超过了mtu(最大数据包大小),则分片;ip将这些数据包传给链路层
链路层写到网卡队列
网卡调用响应中断驱动程序,发送到网络

接收:

数据包从网络到达网卡,网卡接收帧,放入网卡buffer,在向系统发送中断请求
cpu调用相应中断函数,这些中断处理程序在网卡驱动中
中断处理函数从网卡读入内存,交给链路层
链路层将包放入自己的队列,置软中断标志位
进程调度器看到了标志位,调度相应进程
该进程将包从队列取出,与相应协议匹配,一般为ip协议,再将包传递给该协议接收函数
ip层对包进行错误检测,无错,路由
路由结果,packet被转发或者继续向上层传递
如果发往本机,进入链路层
链路层再进行错误侦测,查找相应端口关联socket,包被放入相应socket接收队列。socket唤醒拥有该socket的进程,进程从系统调用read中返回,将数据拷贝到自己的buffer,返回用户态。

 技术分享

 

Listen到客户端socket之后,server就会监听客户端socket的句柄。其他没有被listen的客户端无法与服务端通信。

客户端:

ServerSocket server = new ServerSocket (9527,300); //端口号9527  允许最大连接数300 

Socket socket = server.accept(); 

//可以转化为对stream的操作。之后可通过stream read信息到buffer(Byte)

DataInputStream dis = new DataInputStream(socket.getInputStream()); 

DataOutputStream dos = new DataOutputStream(socket.getOutputStream());  

 

Serverß--byte-àclient

 

Server也可以在accept成功后首先write给client。write/read或者send/recv组合都可以实现交互,send/recv多一组参数。

  • Write函数

   Ssize_twrite(int fd,const void *buf,size_t nbytes);

   Write函数将buf中的nbytes字节内容写入到文件描述符中,成功返回写的字节数,失败返回-1.并设置errno变量。在网络程序中,当我们向套接字文件描述舒服写数据时有两种可能:

   1、write的返回值大于0,表示写了部分数据或者是全部的数据,这样用一个while循环不断的写入数据,但是循环过程中的buf参数和nbytes参数是我们自己来更新的,也就是说,网络编程中写函数是不负责将全部数据写完之后再返回的,说不定中途就返回了!

   2、返回值小于0,此时出错了,需要根据错误类型进行相应的处理。

   如果错误是EINTR表示在写的时候出现了中断错误,如果是EPIPE表示网络连接出现了问题。

Read函数

   Ssize_tread(int fd,void *buf,size_t nbyte)

   Read函数是负责从fd中读取内容,当读取成功时,read返回实际读取到的字节数,如果返回值是0,表示已经读取到文件的结束了,小于0表示是读取错误。

   如果错误是EINTR表示在写的时候出现了中断错误,如果是EPIPE表示网络连接出现了问题。

  • Recv函数和send函数

   Recv函数和read函数提供了read和write函数一样的功能,不同的是他们提供了四个参数。

   Intrecv(int fd,void *buf,int len,int flags)

   Intsend(int fd,void *buf,int len,int flags)

   前面的三个参数和read、write函数是一样的。第四个参数可以是0或者是一下组合:

   MSG_DONTROUTE:不查找表。是send函数使用的标志,这个标志告诉IP,目的主机在本地网络上,没有必要查找表,这个标志一般用在网络诊断和路由程序里面。

   MSG_OOB:接受或者发生带外数据。 表示可以接收和发送带外数据。

   MSG_PEEK:查看数据,并不从系统缓冲区移走数据。是recv函数使用的标志,表示只是从系统缓冲区中读取内容,而不清楚系统缓冲区的内容。这样在下次读取的时候,依然是一样的内容,一般在有过个进程读写数据的时候使用这个标志。

   MSG_WAITALL:等待所有数据。 是recv函数的使用标志,表示等到所有的信息到达时才返回,使用这个标志的时候,recv返回一直阻塞,直到指定的条件满足时,或者是发生了错误。

因为存在阻塞,一般会使用线程来进行阻塞等待。

Select()

确定一个或多个套接口的状态,本函数用于确定一个或多个套接口的状态,对每一个套接口,调用者可查询它的可读性、可写性及错误状态信息,用fd_set结构来表示一组等待检查的套接口,在调用返回时,这个结构存有满足一定条件的套接口组的子集,并且select()返回满足条件的套接口的数目。当返回为-1时,所有描述符集清0。当返回为0时,表示超时。当返回为正数时,表示已经准备好的描述符数。

使用Select能够监视文件描述符的变化情况——读写或是异常。相对的,conncet()、accept()、recv()或recvfrom是阻塞方式。

#include <sys/select.h>

int PASCAL FAR select( int nfds, fd_setFAR* readfds, fd_set FAR* writefds, fd_set FAR* exceptfds, const struct timeval FAR* timeout);

nfds:是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错!在Windows中这个参数的值无所谓,可以设置不正确。

readfds:(可选)指针,指向一组等待可读性检查的套接口。

writefds:(可选)指针,指向一组等待可写性检查的套接口。

exceptfds:(可选)指针,指向一组等待错误检查的套接口。

timeout:select()最多等待时间,对阻塞操作则为NULL。

文件描述符(句柄)在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。

fd_set set;

FD_ZERO(&set); /*将set清零使集合中不含任何fd*/

FD_SET(fd, &set); /*将fd加入set集合*/

FD_CLR(fd, &set); /*将fd从set集合中清除*/

FD_ISSET(fd, &set); /*在调用select()函数后,用FD_ISSET来检测fd是否在set集合中,当检测到fd在set中则返回真,否则,返回假(0)*/

理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
   (1)执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000。
   (2)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)

   (3)若再加入fd=2,fd=1,则set变为0001,0011
   (4)执行select(6,&set,0,0,0)阻塞等待
   (5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。

 

初始化 (socket,bind,listen);
while(1) {
//设置监听读写文件描述符 (FD_*);

FD_ZERO(&mFdSet);

FD_SET(mSock, &mFdSet);
//调用 select;
int err = select(maxFd + 1, &fdSet, NULL, NULL, NULL);

//如果是倾听套接字就绪 , 说明一个新的连接请求建立

if (FD_ISSET(mSock, &fdSet)) {

{

//建立连接 (accept);

int fd = accept(mSock, (struct sockaddr*)&addr, &slen);
//加入到监听文件描述符中去 ;

mClients.push_back(new OemClient(fd));

}
//否则说明是一个已经连接过的描述符
//进行操作 (read 或者 write);

for (unsigned i = 0; i < nClientSize; i++) {

int fd = mClients[i]->GetFd();

/* read header */

err = read(fd, &req, RILC_REQUEST_HEADER_SIZE);

/* read data */

err = read(fd, data, req.length);

}

/* forward to RIL */

m_pRilApp->OnOemRequest(reqId, (void *)data, req.length, (Token)p, (RIL_SOCKET_ID)req.channel);

}

 

  1. FD_ZERO(&fds);  
  2. FD_SET(sock_sev,&fds);  
  3. if (select(maxfdp,&fds,NULL,NULL,&timeOut))  
  4. {         
  5.      cout<<"new connection"<<endl;  
  6.  sock_client = accept(sock_sev, (sockaddr *)&addr_client, &nAddrLen);  
  7.     if (INVALID_SOCKET == sock_client)  
  8.  {  
  9.      std::cout << "Failed to accept." << std::endl;  
  10.   continue;  
  11.  }  
  12.  socketClient[i].socketConn=sock_client;  
  13.  std::cout << "Connection from " << inet_ntoa(addr_client.sin_addr)<< std::endl;  
  14.  socketClient[i].ClientID=i;  
  15.  socketClient[i].socketThread=CreateThread(NULL,0,ThreadRead,&socketClient[i],0,socketClient[i].ThreadID);  
  16.  i++;  
  17. }  

 

长连接

所谓长连接,指在一个TCP连接上可以连续发送多个数据包,在TCP连接保持期间,如果没有数据包发送,需要双方发检测包以维持此连接,一般需要自己做在线维持。 
短连接是指通信双方有数据交互时,就建立一个TCP连接,数据发送完成后,则断开此TCP连接。对于socket来说,不close就是长连接

通常的短连接操作步骤是: 
连接→数据传输→关闭连接;


而长连接通常就是: 
连接→数据传输→保持连接(心跳)→数据传输→保持连接(心跳)→……→关闭连接; 

长连接的意义:每个TCP连接都需要三步握手,这需要时间,如果每个操作都是先连接,再操作的话那么处理速度会降低很多

HTTP长连接:系统通讯连接建立后就一直保持,不关闭socket,保持TCP连接不断开(不发RST包、不四次握手)。
对于http协议来说,在消息头上加入:“Connection:keep-alive”,那么消息发送后仍然保持打开状态。

使用长连接之后,客户端、服务端怎么知道本次传输结束呢?两部分:1是判断传输数据是否达到了Content-Length指示的大小;2动态生成的文件没有Content-Length,它是分块传输(chunked),这时候就要根据chunked编码来判断,chunked编码的数据在最后有一个空chunked块,表明本次传输数据结束。

 

TCP的keep alive是心跳包,检查当前TCP连接是否活着;HTTP的Keep-alive是要让一个TCP连接活久点(不关闭TCP连接)。它们是不同层次的概念。

TCP keep alive的表现:当一个连接“一段时间”没有数据通讯时,一方会发出一个心跳包(Keep Alive包),如果对方有回包则表明当前连接有效,继续监控。

Java nio

传统阻塞IO的缺点:

1. 当客户端多时,会创建大量的处理线程。且每个线程都要占用栈空间和一些CPU时间
2. 阻塞可能带来频繁的上下文切换,且大部分上下文切换可能是无意义的。

 

Mina集成了java nio。

Java NIO非堵塞技术原理:

1. 由一个专门的线程来处理所有的 IO 事件,并负责分发。(就阻塞这一个reactor)
2. 事件驱动机制:事件到的时候触发,而不是同步的去监视事件。
3. 线程通讯:线程之间通过 wait,notify 等方式通讯。保证每次上下文切换都是有意义的。减少无谓的线程切换。

原理图:(每个线程的处理流程大概都是读取数据、解码、计算处理、编码、发送响应。)

 技术分享

技术分享

 

Socket的Channel在Selector上注册某一种动作,Selector通过select操作,监视所有在该Selector注册过的Channel的对应的动作,一旦轮询到一个channel有所注册的事情发生,比如数据来了,他就会站起来报告,交出一把钥匙(SelectionKeys,用来得到某个注册的ServerSocketChannel/SocketChannel),让我们通过这把钥匙来读取这个channel的内容。

事件名对应值:

  服务端接收客户端连接事件SelectionKey.OP_ACCEPT(16)

  客户端连接服务端事件SelectionKey.OP_CONNECT(8)

  读事件SelectionKey.OP_READ(1)

  写事件SelectionKey.OP_WRITE(4)

 技术分享

 

 技术分享

Selector管理所有ServerSocketChannel(一个客户端可以用一个ServerSocketChannel保持通信),只需要阻塞selector,当ServerSocketChannel有消息进入时,才调用他们的accept()。具体的数据读写活由SocketChannel来做,用完的SocketChannel需要用close()关闭。

网络2