网络层编写有点网编经验的同学都能做出来,但正在开发时却遇到几个需要注意的地方。
1.在发送数据时, intcnt = (int)write(self->_socketFD, data, size); 并不会立刻发送,会有一定时间的延时,经调研是tcp默认开启了nagle算法。
此算法让连接里同一时间只容许存在一个未被确认的数据段,当客户端给服务端发消息时,发消息A,此时服务端收到A后会返回个ack,而此时客户端又发了个消息B,如此时A的ack没有收到时,客户端会先把B存到发送缓存里,等收到ack或超过一定时长后再发送B。
同时服务端也并不会立刻返回ack,而是会在一定时长内等待发送其他应答消息,和ack一起发送。
这么做的目的是充分利用网络带宽,TCP总是希望尽可能的发送足够大的数据.
但是在IM这种实时性比较高的业务场景中,此算法略显鸡肋,不如禁掉。
不过我选择的是另外一种方法:刷新发送缓存。
self->file = fdopen (self->_socketFD, "w+");
intcnt = (int)write(self->_socketFD, data, size);
fflush(self->file);
这样能实现和禁掉nagle算法相同的功能,并且主动权在开发者手里,可主动调用。
2.读取数据(int)cnt = read(self->_socketFD,buffer,500);
有时无法一次性读到cnt = 500,猜想是因为tcp的流量控制,某一时刻接收缓存区空间不够,接收窗口告知发送窗口空间不够,发送方无法一次发送全部数据。
所以要多次read,以读到指定的length。
伪代码如:
int needsize = 500;
int alreadyGetSize = 0;
while(1)
int cnt = (int) read(self->_socketFD, buffer,needsize);
alreadyGetSize += cnt;
if (alreadyGetSize == needsize ) {
printf("获取完毕");
}
3. connect超时机制,做到这里时,因为connect默认是阻塞式的,完全不处理大概阻塞个几分钟都有可能。而我们所能容忍的是5秒,如何让connect在阻塞时结束是关键。
而GCDAsyncsocket做法是,额外开一个线程定时器,如在5s内connect成功则关掉定时器,如5s到达时则close socket。
这么做是可以的,但我的设想是,如果能在一个线程里去完成超时任务,节省线程开销岂不是更好,而且GCDAsyncsocket 里 用gcd实现timer,block嵌套较深,复杂难懂。
既然connect是阻塞的,那么改成非阻塞就好了。
//为设置超时时间,套接字阻塞->非阻塞
if(fcntl(self->_socketFD,F_SETFL,fcntl(self->_socketFD,F_GETFL) |O_NONBLOCK)<0) {
NSLog(@"FCNTL Error");
return Connect_Failed;
}
//建立三路握手
intconnected =connect(self->_socketFD, (conststructsockaddr*)[databytes], (socklen_t)[datalength]);
然后用select函数监听套接字状态变化
//设置connect阻塞时间
tm.tv_sec = ConnectTimeOut;
tm.tv_usec=0;
FD_ZERO(&sockfd_set);
FD_SET(self->_socketFD,&sockfd_set);
selectVal =select(self->_socketFD+1,NULL, &sockfd_set,NULL, &tm);
switch(selectVal) {
case-1: {
NSLog(@"Get Select Connect Failed");
ret =Connect_Failed;
}
case0: {
ret =Connect_Failed;
NSLog(@"Time Out To Connect Failed");
}
break;
default: {
if(FD_ISSET(self->_socketFD,&sockfd_set)) {
if(getsockopt(self->_socketFD,SOL_SOCKET,SO_ERROR, &error, (socklen_t*)&len) <0) {
ret =Connect_Failed;
}
NSLog(@"error=%d\n",error);
if(error ==0) {
ret =1;
}
else{
ret =0;
errno= error;
NSLog(@"error=%d\n",error);
}
}
}
break;
}
最后别忘记把套接字再改回阻塞式的
fcntl(self->_socketFD, F_SETFL, fcntl(self->_socketFD,F_GETFL) &~ O_NONBLOCK);
这样在当前线程就可完成连接超时机制。
4.异常close 问题
在soul上线一段时间之后,会发现一个很奇怪的bug,我对对方发的消息全失败,却能收到对方的消息。
经调研发现,当客户端A异常关闭close时,实际上只关闭了socket的写操作,同时发送finA表示关闭了A到B的数据传送,B收到finA后回个ack代表收到,然后B再回A一个finB,代表B关闭了到A的数据传递,而假设因为网络异常原因,finA没有发送到B,或者B没有发送finB会如何呢? 很可能会出现上面描述的bug,反之亦然。
目前的解决方案是,当A发送close后,立刻重连,当服务端收到重连请求后,立刻根据用户Id查询此用户有无重复连接在保持中,如 有则主动断开,去进行新的连接。
难点在于后端需要为每个用户维持一个连接记录,建立一个连接则把这个连接标记,记录下来。
当下一个连接请求到来时,close掉连接记录里的所有连接,再去应答新连接。