首页 > 代码库 > 网络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
- //这一步很关键,就是获取init.rc中配置的名为 "htfsk" 的socket
- fdListen = android_get_control_socket(SOCKET_NAME);
- …
- //开始监听
- ret = listen(fdListen, connect_number);
- //等待Socket客户端发启连接请求
- new_fd = accept(fdListen, (struct sockaddr *) &peeraddr, &socklen);
- …
- while(1){
- 10. //循环等待Socket客户端发来消息
- }
- //发送消息回执给Socket客户端
- if(send(new_fd,buff,strlen(buff),0)==-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); FD_ZERO(&mFdSet); FD_SET(mSock, &mFdSet); //如果是倾听套接字就绪 , 说明一个新的连接请求建立 if (FD_ISSET(mSock, &fdSet)) { { //建立连接 (accept); int fd = accept(mSock, (struct sockaddr*)&addr, &slen); mClients.push_back(new OemClient(fd)); } 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); }
|
- FD_ZERO(&fds);
- FD_SET(sock_sev,&fds);
- if (select(maxfdp,&fds,NULL,NULL,&timeOut))
- {
- cout<<"new connection"<<endl;
- sock_client = accept(sock_sev, (sockaddr *)&addr_client, &nAddrLen);
- if (INVALID_SOCKET == sock_client)
- {
- std::cout << "Failed to accept." << std::endl;
- continue;
- }
- socketClient[i].socketConn=sock_client;
- std::cout << "Connection from " << inet_ntoa(addr_client.sin_addr)<< std::endl;
- socketClient[i].ClientID=i;
- socketClient[i].socketThread=CreateThread(NULL,0,ThreadRead,&socketClient[i],0,socketClient[i].ThreadID);
- i++;
- }
长连接
所谓长连接,指在一个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