java做服务器向Cshadowsock xp客户端端发送数据异常的sock通讯异常问题

Socket通信——C++服务器端和Java客户端-爱编程
Socket通信——C++服务器端和Java客户端
一句话来说就是,C++和Java 通过socket进行通信、数据传输,通过发送“字节流”即可。
字节对于C++和java来说是通用的,但是传输的过程有许多问题需要注意,我为了弄清楚这个过程,查了一些资料,做了一些整理。
&不了解C++ socket编程,可以看这篇博客:
Linux 下:socket通信(Linux下,C/C++语言):http://blog.csdn.net/giantpoplar/article/details/
Windows下:winsock:http://blog.csdn.net/giantpoplar/article/details/
不了解Java socket编程,可以看这篇博客:
Java socket通信:http://blog.csdn.net/giantpoplar/article/details/
不了解字节数组和基本数据类型的转换,可以看这篇博客
byte[]和 整形、浮点型数据的转换-java代码:http://blog.csdn.net/giantpoplar/article/details/
在服务器向客户端发数据的时候,可能采取以下两种方式
服务器端直接发送struct
如果在编程时在C++端直接发送一个结构体,需要把这个结构体的指针强制转换为char*,通过一个个的字节传输
Msg message;
retVal = send(Client, (char*)&message,sizeof(Msg), 0);
如果采用这种方式,在客户端收到数据后进行解释,需要对C++对象的内存布局有一个简单的了解,同样,如果你不了解,可以看这篇:简单C++对象的内存布局:http://blog.csdn.net/giantpoplar/article/details/
另外如果你的服务器端用了java,以ObjectOutputStream的方式写出一个对象,需要对java对象的内存布局有所了解,同样,如果你不了解,可以看这篇:&简单Java对象的内存布局:http://blog.csdn.net/giantpoplar/article/details/
&服务器端把struct的每一个成员单独发送
还有一种方式是不把整个结构体整个发过去,而是一个一个变量发过去,这时候服务器和客户端两边要知道这些变量的相互顺序,一个一个一次接受即可,这就涉及到字节数组和基本数据类型之间的转换。
&这个时候要注意服务器和客户端整数的表示方式是否一样,是不是都是大端法或者都是小端法,浮点数表示是否都符合IEEE 754规范。否则两边要协调好,或者浮点数直接传字符串,虽然会带来效率损失,但是可以统一起来。
这两种问题都有缺点,首先就是大端表示,小端表示,不同的处理器采用的表示方式可能不同,可能当前运行正常,但可能带来的潜在的错误。
第二就是如果struct里面包含指针,它所指向的数据并不在struct的实例里面,可能会和使用者的想法相违背,这种情况下使用“序列化”可能是更好的选择,这里找到一个简单的介绍q.com/cn/news/2014/06/flatbuffers。
我遇到的一些问题
服务器端在发送数据时一次发送的大小不要太大,比如我在发送一个538字节的Msg时,发生了传输异常的问题,我在本机测试正常,不再同一台机器上时就会出问题。
要注意:发送端和接收端接受的数据大小要相同,比如一次发128字节,接受也要一次接受128字节,否则容易造成丢包,粘包之类的问题,数据传输异常;
一次发的不要太大,太大也容易出问题;
要想更可靠,自己可以再定义一个协议,每次发数据时包含进去包的长度,校验码等信息
下面附上我最近在写的一个程序,基本功能有了,但还不是很完善
因为我的服务器和客户端肯定都是小端模式存贮整数,所以就暂时没考虑传输时的字节顺序的问题
写一个Message类是为了将来把一些方法放进去用着比较方便。
Java客户端
import java.io.IOE
import java.io.InputS
import java.io.ObjectInputS
import java.io.OutputS
import java.net.ServerS
import java.net.S
import java.net.UnknownHostE
public class JavaClient{
public static void main(String[] args) throws UnknownHostException,
IOException, ClassNotFoundException {
Socket s = new Socket(&127.0.0.1&, 8899);
InputStream is = s.getInputStream();
byte msg[] = new byte[536];
while (is.read(msg, 0, 536) & -1) {
Message m = new Message(msg);
if(m.guesture!=0)System.out.println(m.guesture);
public class Message {
float left_hand[] = new float[66];//左手关节数据[X0 Y0 Z0 X1 Y1 Z1...X21 Y21 Z21]
float right_hand[] = new float[66];//右手关节数据
//是否检测到左手
1 为检测到, 0为未检测到
//是否检测到右手
short alert_//警告类型
public Message(byte[] msg){//从字节数组构造一个message对象
for(int i=0 ; i&66 ; i++){
left_hand[i] = byte2float(msg, 4* i);
for(int i=0 ; i&66 ; i++){
right_hand[i] = byte2float(msg,264 + 4 * i);
getShort(msg, 528);
getShort(msg, 530);
getShort(msg, 532);
alert_type
getShort(msg, 534);
//数组顺序按照“小端顺序”
private float byte2float(byte[] b, int index) {//4个字节转float
l = b[index + 0];
l |= ((long) b[index + 1] && 8);
l |= ((long) b[index + 2] && 16);
l |= ((long) b[index + 3] && 24);
return Float.intBitsToFloat(l);
private short getShort(byte[] bytes,int index){//两个字节转short
return (short) ((0xff & bytes[index+0]) | (0xff00 & (bytes[index+1] && 8)));
C++服务端
#include&Windows.h&
#include&iostream&
#include&string.h&
#pragma comment(lib, &ws2_32.lib&)
#ifndef MSG_
#define MSG_
struct Msg
pxcF32 left_hand[66];
pxcF32 right_hand[66];
pxcI16 alert_
};//消息结构
Int main(){
//WSADATA变量
//服务器套接字
//客户端套接字
SOCKADDR_IN
//服务器地址
//初始化套接字动态库
if (WSAStartup(MAKEWORD(2, 2), &wsad) != 0)
std::printf(&初始化套接字动态库失败!\n&);
//创建套接字
Server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET == Server)
std::printf(&创建套接字失败!\n&);
WSACleanup();//释放套接字资源;
//服务器套接字地址
addrServ.sin_family = AF_INET;
addrServ.sin_port = htons(8899);
addrServ.sin_addr.s_addr = INADDR_ANY;
//绑定套接字
retVal = bind(Server, (LPSOCKADDR)&addrServ, sizeof(SOCKADDR_IN));
if (SOCKET_ERROR == retVal)
std::printf(&绑定套接字失败!\n&);
closesocket(Server);
//关闭套接字
WSACleanup();
//释放套接字资源;
return -1;
//开始监听
retVal = listen(Server, 1);
if (SOCKET_ERROR == retVal)
std::printf(&监听失败!\n&);
closesocket(Server);
//关闭套接字
WSACleanup();
//释放套接字资源;
return -1;
//接受客户端请求
sockaddr_in addrC
int addrClientlen = sizeof(addrClient);
Client = accept(Server, (sockaddr FAR*)&addrClient, &addrClientlen);
std::printf(&accept a socket\n&);
if (INVALID_SOCKET == Client)
std::printf(&接受客户端请求失败!\n&);
// closesocket(Server);
//关闭套接字
// WSACleanup();
//释放套接字资源;
// return -1;
//发送客户端数据
while (true){
WaitForSingleObject(full_sem, INFINITE);//down操作
//WaitForSingleObject(mutex, INFINITE);//多个消费者需要加互斥信号量
Msg message = MsgQueue[head];
head = (head + 1) % QUEUE_LENGTH;
//ReleaseSemaphore(mutex, 1, NULL);//up操作
ReleaseSemaphore(empty_sem, 1, NULL);//up操作
retVal = send(Client, (char*)&message, sizeof(Msg), 0);
if (SOCKET_ERROR == retVal)
std::printf(&接收客户端请求失败!\n&);
//closesocket(Server);
//关闭套接字
closesocket(Client);
//关闭套接字
//return -1;
Sleep(100);
//std::printf(&%s\n&, buf);
//输出来自Client的字符串
//closesocket(Server);
//关闭套接字
closesocket(Client);
//关闭套接字
closesocket(Server);
WSACleanup();
//释放套接字资源;
本文由giantpoplar发表于CSDN
文章地址&http://blog.csdn.net/giantpoplar/article/details/
转载请保留本说明
版权声明:本文为博主原创文章,未经博主允许不得转载。
版权所有 爱编程 (C) Copyright 2012. . All Rights Reserved.
闽ICP备号-3
微信扫一扫关注爱编程,每天为您推送一篇经典技术文章。16069人阅读
音乐检索(7)
最近正在开发一个基于指纹的音乐检索应用,算法部分已经完成,所以尝试做一个Android App。Android与服务器通信通常采用HTTP通信方式和Socket通信方式。由于对web服务器编程了解较少,而且后台服务器已经采用原始socket实现与c客户端通信,这就要求Android客户端也采用socket实现。所以在开发Android app时采用了原始socket进行编程。
由于算法是用C语言实现的,而Android应用一般是Java开发,这就不可避免得涉及Java和C语言之间的通信问题。一种方案是在客户端采用JNI方式,上层UI用Java开发,但是底层通信还是用C的socket完成。这种方案需要掌握JNI编程,对不少Java开发者是个障碍。为了减小开发难度,最好的方案是直接用Java socket与C socket进行通信。但是这种方案也有问题,最大的问题在于API和数据格式的不统一。本人在本科曾尝试利用Java和c的socket进行通信,发现根本无法传递数据,一度认为这两种socket之间无法通信。今天重拾旧问题,必须一次性地完美地解决Java和C之间的socket通信问题。在此可以先将实现总结为1句话:通信全部用字节实现。
在介绍Java和c之间的socket通信之前,首先将音乐检索大概介绍一下,更详细的内容可参考。基于指纹的音乐检索就是让用户录制一段正在播放的音乐上传服务器,服务器通过提取指纹进行检索获得相应的歌名返回给用户,就这么简单。简单的工作原理如图一。所以在该应用中,socket通信主要涉及两个方面:客户端向服务器发送文件和服务器向客户端发送结果两部分。下面先介绍服务器部分。
图1 音乐检索的简单工作原理示意图
1 服务器设计
服务器端采用C socket进行通信,同时为了能响应多用户请求,服务器端需要采用多线程编程。为了专注于socket通信,已经将无关代码去掉,首先看main方法。
typedef struct
int client_
void get_ip_address(unsigned long address,char* ip)
sprintf(ip,&%d.%d.%d.%d&,address&&24,(address&0xFF0000)&&24,(address&0xFF00)&&24,address&0xFF);
int main()
int server_
int server_
struct sockaddr_in server_
server_sockfd=socket(AF_INET,SOCK_STREAM,0);
server_address.sin_family=AF_INET;
server_address.sin_addr.s_addr=htonl(INADDR_ANY);
server_address.sin_port=htons(9527);
server_len=sizeof(server_address);
bind(server_sockfd,(struct sockaddr*)&server_address,server_len);
listen(server_sockfd,MAX_THREAD);
while(true)
int client_
struct sockaddr_in client_
int client_
char ip_address[16];
client_arg*
client_len=sizeof(client_address);
client_sockfd=accept(server_sockfd,(struct sockaddr*)&client_address,(socklen_t*)&client_len);
args=(client_arg*)malloc(sizeof(client_arg));
args-&client_sockfd=client_
get_ip_address(ntohl(client_address.sin_addr.s_addr),ip_address);
printf(&get connection from %s\n&,ip_address);
//////////////////////create a thread to process the query/////////////////////
pthread_t client_
pthread_attr_t thread_
res=pthread_attr_init(&thread_attr);
if(res !=0)
perror(&Attribute creation failed&);
free(args);
close(client_sockfd);
res=pthread_attr_setdetachstate(&thread_attr,PTHREAD_CREATE_DETACHED);
if(res !=0)
perror(&Setting detached attribute failed&);
free(args);
close(client_sockfd);
res=pthread_create(&client_thread,&thread_attr,one_query,(void*)args);
if(res !=0)
perror(&Thread creation failed&);
free(args);
close(client_sockfd);
pthread_attr_destroy(&thread_attr);
服务器端采用标准的TPC(threadper connection)架构,即服务器每获得一个客户端请求,都会创建一个新的线程负责与客户端通信,具体的任务都在每一个线程中完成。这种方式有个缺点,就是存在线程的频繁创建和删除,所以还可以将accept函数放入每一个线程中进行独立监听(这种方式需要加锁)。需要注意的是我们需要设置线程属性为detached,表示主线程不等待子线程。下面介绍每个线程具体完成的任务:
void get_time(char* times)
struct tm*
timep=time(NULL);
p=gmtime(&timep);
sprintf(times,&%d-%02d-%02d-%02d-%02d-%02d&,p-&tm_year+1900,p-&tm_mon+1,p-&tm_mday,
p-&tm_hour+8,p-&tm_min,p-&tm_sec);
int recv_file(char* path,int client_sockfd,int file_length)
char buffer[1024];
fp=fopen(path,&wb&);
if(fp==NULL)
perror(&Open file failed&);
return -1;
while((read_length=recv(client_sockfd,buffer,1023,0))&0)
buffer[read_length]='\0';
fwrite(buffer,1,read_length,fp);
file_length-=read_
if(!file_length)
fclose(fp);
printf(&write to file %s\n&,path);
void* one_query(void* arg)
char file_name[32];
char path[64]=&./recv_data/&;
char length[10];
int file_length=0;
client_arg* args=(client_arg*)
int sockfd=args-&client_
get_time(file_name);
strcat(file_name,&.wav&);
strcat(path,file_name);
/////////////1.receive file length//////////////////
recv(sockfd,length,10,0);
file_length=atoi(length);
printf(&file length is %d\n&,file_length);
/////////////2.receive file content//////////////////
if(recv_file(path,sockfd,file_length)==-1)
perror(&receive file failed&);
close(sockfd);
pthread_exit(NULL);
//////////////3.search the fingerprint library, and get the expected music id//////////////
int count=match(&list);
char result_to_client[2000];
for(int i=0;i&i++)
if(list[i].confidence&0.4)
memset(length,0,sizeof(length));
memset(result_to_client,0,sizeof(result_to_client));
/////////////////4. retrieve the database to get detailed information //////////////
MYSQL_RES* res=select_music_based_on_id(list[i].id);
row_result* row_res=fetch_row(res);
sprintf(result_to_client,&%s,%s,%s,%d,%d,%lf&,row_res-&name,row_res-&artist,row_res-&album,list[i].score,list[i].start_time,list[i].confidence);
/////////////////5. Send a retrieval flag(1:success,0:fail)//////////////////////
sprintf(length,&%d&,1);
send(sockfd,length,10,0);
/////////////////6. Send the result////////////////////////////////////
send(sockfd,result_to_client,2000,0);
free_result(res);
free_row(row_res);
memset(length,0,sizeof(length));
sprintf(length,&%d&,0);
send(sockfd,length,10,0);
free(list);
close(sockfd);
pthread_exit(NULL);
one_query函数实现了每个线程与客户端通信的代码。代码核心的部分可以表示为六步:1. 从客户端读取录制音频的长度;2. 读取实际的音频,并保存到文件,文件以当前时间命名;3. 检索指纹服务器,获得检索的音乐id;4. 如果检索结果置信度高,则利用检索到的id访问数据库获得更加详细的音乐信息;5. 给用户发送一个成功/失败标注;6. 如果检索成功,发送具体的音乐信息。
1.1 读取文件长度
在第一步读取音频长度时,我们采用了原始socket中的recv函数。该函数原型为:
Int recv(intsocket, void *buff, int length, int flags)
接收数据用void* 获取,我们可以用char数组按照字节来读取,读取之后再解析。需要注意的一点是参数中传递的长度必须大于客户端可能传递过来的长度,在此我们用10字节来表示传递的上限(int型最大约为4*109,需要10位,加上’\0’需要11位,但是音频长度远小于最大的int值,所以只分配10位)。读到的char数组之后利用atoi转化为实际的int型整数。网上很多博客在介绍Java和C之间的socket通信时会涉及复杂的大小端问题,由于我们将所有的数据都转成字节数组传递,所以不存在这个问题。
1.2 读取音频文件
&&&&&& 音频文件的读取在recv_file中实现。读取的核心还是按照字节流来完成,每次读取1023字节的数据,然后写入文件。这里有两点需要注意:首先recv读取的长度和我们指定的长度可能不一致,也即返回的长度小于1023,我们需要以返回的长度为准;分配的数组长度是1024,但是我们每次读取的数据最长只能为1023,这是因为我们需要在读取数据的最后添加一个’\0’标记,用来标记数据的末尾。读取结束的标志是达到之前传递过来的文件长度。
1.3 检索指纹库
&&&&&& 该步骤在获得完整的音频文件之后,就对该文件提取指纹然后检索指纹库,原理可参考,在此不再赘述。检索的结果是一个音乐的top5列表。每一项结果都有检索得到的音乐id和相应的置信度。
1.4 访问数据库
&&&&&& 该步骤在top 5列表中有置信度大于0.4的音乐时执行。利用检索得到的id去访问数据库,获得音乐的名字和作者等信息。
1.5 发送flag标记
&&&&&& 在发送具体的信息之前先发送一个标记,表示此次检索是成功还是失败,方便客户端显示。如果成功,发送标记‘1’,失败则发送标记‘0’。发送时,并不是直接发送一个int型的整数,而是首先利用sprintf将整型变为char型字符串,交给客户端去解析。发送函数采用原始socket中的send函数,原型为:
Int send(int socket, const void * buff, int length, int flags)
1.6 发送音乐信息
&&&&&& 当检索到对应的音乐时,则把具体的音乐信息发送给客户端。这里还是利用sprintf将信息都打印到字符串中。可以看出,为了与Javasocket通信,所有的数据传递都被转换成char*字符串。
2 客户端实现
&&&&&& 在介绍客户端之前,先把代码贴出来:
import java.io.*;
import java.net.*;
public class Client
void query(String file,String ip,int port)
FileInputStream fileInputS
DataInputStream netInputS
DataOutputStream netOutputS
byte[] buffer=new byte[1023];
byte[] readLen=new byte[10];
byte[] readResult=new byte[2000];
int result_count=0;
File f=new File(file);
if(f.exists())
fileLength=(int)f.length();
System.out.println(&No such file&);
fileInputStream=new FileInputStream(file);
sc=new Socket(ip,port);
netInputStream=new DataInputStream(sc.getInputStream());
netOutputStream=new DataOutputStream(sc.getOutputStream());
/////////////////////1.send file length//////////////////////
netOutputStream.write(Integer.toString(fileLength).getBytes());
/////////////////////2. send file///////////////////////////
while((len=fileInputStream.read(buffer))&0)
netOutputStream.write(buffer,0,len);
////////////////3. read result symbol///////////////////////////////
netInputStream.read(readLen);
while(((char)readLen[0])=='1')
/////////////////////4. Read result//////////////////////////////
netInputStream.read(readResult);
String result=new String(readResult);
String[] ss=result.split(&,&);
int score=Integer.parseInt(ss[3]);
int startTime=Integer.parseInt(ss[4]);
double confidence=Double.parseDouble(ss[5]);
System.out.println(&name:&+ss[0].trim());
System.out.println(&artist:&+ss[1].trim());
System.out.println(&album:&+ss[2].trim());
System.out.println(&score:&+score);
System.out.println(&startTime:&+startTime);
System.out.println(&confidence:&+confidence);
result_count++;
netInputStream.read(readLen);
if(result_count==0)
System.out.println(&No match music&);
fileInputStream.close();
netInputStream.close();
netOutputStream.close();
sc.close();
catch(Exception e)
e.printStackTrace();
public static void main(String[] args)
Client client=new Client();
client.query(args[0],args[1],9527);
与服务器端相对应,客户端的流程主要分为四步:1. 发送文件长度;2. 发送文件内容;3. 读取标记;4. 读取检索结果。在此,读取文件采用FileInputStream流,网络通信采用DataInputStream和。
2.1 发送文件长度
&&&&&& Java在发送int型时,也需要转换成字符串,在此我们先用Integer封装类获取int型的字符串表示,然后利用String类的getBytes函数获得其字节数组。最后利用DataOutputStream的write函数发送给服务器。
2.2 发送文件
&&&&&& 发送文件的过程是:首先从文件中读取固定长度的内容,然后再利用write函数发送同等长度的字节数组。
2.3 读取标记
&&&&&& 发送完文件之后,客户端就等着从服务器端获取检索结果。服务器首先返回一个0/1标记。由于该标记有效内容只有一个字节,所以我们可以通过读取第0个字节的内容来判断检索是否成功。读取是通过DataInputStream的read函数完成,读取的内容会放在原始的字节数组中。
2.4 读取音乐信息
&&&&&& 如果检索成功,服务器在发送成功标记之后还会将完整的音乐信息发送过来。读取还是利用DataInputStream的read函数。读取的内容比较复杂,我们首先将字节数组转换成字符串,然后利用split函数解析出每一部分内容。之后就可以在Android UI界面中显示。
&&&&&& 在亲自完成Java和c之间的socket通信之后,感觉也没有那么复杂。其实核心就一点:所有的数据类型都转换成字节数组进行传递。C端用recv和send函数就行,Java端用read和write就行,就这么简单。
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:472827次
积分:4375
积分:4375
排名:第4486名
原创:62篇
转载:19篇
评论:188条
阅读:78061
阅读:36811在以TCP为连接方式的服务器中,为什么在服务端设计当中需要考虑心跳?
这个心跳包除了告知服务端我在线,还有其他作用吗?比如有答案提到的运营商主动断掉没有数据报的网络连接?
按时间排序
回错题目了,编辑掉。。。无视我
我遇到一个socket服务器端和lvs服务器端的问题,我在不引入lvs时候,c#编写的iocp模型,可以稳定运行,可是放加入lvs上就非常容易异常退出,不执行任何的捕获异常的代码,很是奇怪,有没有哪位大神给指导下解决方向
只要两端地址、端口没变,空闲/中断再长时间都能恢复,但问题是:1 万恶的NAT 连接空闲时间过长时 地址、端口被回收2 移动互联网 终端接入点改变导致地址、端口变化此外,KeepAlive包的默认间隔长达2小时(RFC1122),基本没卵用……
题主去搜一下“tcp half-open connection”大概就知道了,“心跳”至少最初就是为了解决这个困扰的。关于这一点 @韦易笑 的回答已经说的挺全面了。简单说就是,对于已建立连接的通讯两段,处于等待接收的一方一般发现不了连接意外中断故障。只有发送方在一或若干次没有回应的失败的发送后才能确定网断了。这种情况会使得接收端傻等。这就是困扰。心跳就是接收端时不时也发送一把,避免傻等。这一般是通讯底层的工作。应用程序/系统当然也可以自定义心跳机制,并自行赋予更多功能。这是程序设计者自己决定的事了。
通常情况下是为了防止连接被NAT禁掉。
短链接的话可以设计专门的心跳包定时传输。长连接的话没必要自行实现,可以用tcp协议自身的心跳(keep-alive)。长连接为什么还需要心跳?连接可能因为以下情形被断开:关闭连接,关闭程序(友好的和异常的),关闭计算机(友好的和异常的),网络硬件故障(拔网线)。其中拔网线和停电关机,是没有机会断开连接的,这时候就要靠心跳了。
你ssh连上服务器的情况下,拔掉网线,你任然检测不到网络断开了(没有FIN),这时候把网线插回去,敲两下键盘,终端一样有反应,链接还活着。因为tcp的链接是否有效,依赖链接两端的状态确定,你在你机器上拔掉网线,你是知道这件事情的,但是中间网络设备拔掉网线,或者出现什么问题,你完全无法得知链接是否还有效,依赖tcp本身的keepalive机制需要半个小时以上才能检测得出来。所幸,keepalive如今也可以设置检测时间了:/* ikeepalive: tcp keep alive option */
int ikeepalive(int sock, int keepcnt, int keepidle, int keepintvl)
int enable = (keepcnt & 0 || keepidle & 0 || keepintvl & 0)? 0 : 1;
unsigned long value;
#if (defined(WIN32) || defined(_WIN32) || defined(_WIN64) || defined(WIN64))
#define _SIO_KEEPALIVE_VALS _WSAIOW(IOC_VENDOR, 4)
unsigned long keepalive[3], oldkeep[3];
OSVERSIONINFO info;
int candoit = 0;
info.dwOSVersionInfoSize = sizeof(info);
GetVersionEx(&info);
if (info.dwPlatformId == VER_PLATFORM_WIN32_NT) {
if ((info.dwMajorVersion == 5 && info.dwMinorVersion &= 1) ||
(info.dwMajorVersion &= 6)) {
candoit = 1;
value = enable? 1 : 0;
isetsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, (char*)&value,
sizeof(value));
if (candoit && enable) {
int ret = 0;
keepalive[0] = enable? 1 : 0;
keepalive[1] = ((unsigned long)keepidle) * 1000;
keepalive[2] = ((unsigned long)keepintvl) * 1000;
ret = WSAIoctl((unsigned int)sock, _SIO_KEEPALIVE_VALS,
(LPVOID)keepalive, 12, (LPVOID)oldkeep, 12, &value, NULL, NULL);
if (ret == SOCKET_ERROR) {
return -1;
return -2;
#elif defined(SOL_TCL) && defined(TCP_KEEPIDLE) && defined(TCP_KEEPINTVL)
value = enable? 1 : 0;
isetsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, (char*)&value, sizeof(long));
value = keepcnt;
isetsockopt(sock, SOL_TCP, TCP_KEEPCNT, (char*)&value, sizeof(long));
value = keepidle;
isetsockopt(sock, SOL_TCP, TCP_KEEPIDLE, (char*)&value, sizeof(long));
value = keepintvl;
isetsockopt(sock, SOL_TCP, TCP_KEEPINTVL, (char*)&value, sizeof(long));
#elif defined(SO_KEEPALIVE)
value = enable? 1 : 0;
isetsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, (char*)&value, sizeof(long));
return -1;
如今,大部分平台下,确实可以不用自己做心跳了,直接调整下KeepAlive的参数到你期望的水平即可。然而如你所见,代码中大量的条件判断。KeepAlive参数调整的实现对平台依赖很大,并非所有平台都支持这样的,你如果能够确定自己代码运行的平台,那么放心大胆的用这个keepalive。你如果无法确定平台,不想依赖平台的keepalive,又需要自己精确控制链接超时,那么请自己实现心跳。
在如下情况下,即:1,服务器不主动给客户端发包。2,客户端非正常关闭连接(比如宕机,断网)。情况下,服务器端将一直维护tcp连接的状态,而与之相关的系统资源(tcp端口,内存)将一直被占用。所以需要服务器每隔一段时间主动给客户端发个包看看对方是否还在~另外,tcp的keepalive的最小检测时长是2小时,对于很多应用而言太长了~再加上心跳又很容易实现,所以一般也就顺手做了。
即使tcp连接没有问题,也不意味着应用层服务正常。好比两个人打电话,电话接通,两边的人睡着了。
一句话给你讲明白:正如TCP层需要自己的心跳一样,应用层也需要自己的心跳,心跳的本质不是探测下层服务是否存活,而是探测对端同层服务是否存活。我再讲一遍:应用层心跳是为了检测对端应用层是否存活(当然也顺便检测了所有下层的存活)
首先自我检讨下,刚几位大神提醒得对,我表达得不是很好啊!我更正下回答,本人技术有限,欢迎大家批评指正!可以防假死,现在很多路由器都会对闲置的网络连接进行管理的!导致服务器上的临时生成的与client通讯的端口可能会发生改变!但是该client却是连接的!那么就有可能找不到client端,从而不能正确通信!而实际上客户端确实是连接的!这就是一种所谓的假死!我们公司的wifi智能插座的服务器,是用tcp/ip通讯的!我记得我在linux服务器上用命令抓包时,我深有体会啊!初出茅庐,不知说得对不对,走了好多坑坑路!
你一定没有遇到过一个连接还在,但是收发消息包都失效的情况吧,当你的服务器上被这些连接所占据,就知道一个逻辑层的心跳是多么的必要了。外网的情况,简直就是神鬼莫测……
tcp 协议的连接状态其实可以看成一个状态机,当客户端和服务端建立连接后,客户端和服务端的状态就变成established,仅此而已。不去主动改变状态,则这个以established 状态是保持着的。这意味即使服务端突然消失了,客户端的状态依然是established。而通过定时心跳,客户端可以知道服务端不见了,从而尝试重连或者释放资源。
理想网络环境下TCP本身的心跳就够了,但由于LVS、运营商HTTP劫持等现实中各种HTTP、TCP劫持的存在,TCP层面的心跳不能保证是真正的服务器发送的,所以要在应用层增加“心跳”
双端连接过程中,如果任意一方意外崩溃、当机、网线断开或路由器故障等"非正常连接",另一方无法得知tcp连接已经失效。解决方案就是实现心跳机制 (应用层,灵活,开发者自己控制检测)。tcp协议栈的keepalive根据需求场合去取舍(不可控,有限制)。补充:举个例子:连接丢失,tcp不会通知应用程序,client断线,server的tcp连接不会检测到断线,而是一直连接中,server还要去维护client的连接,执行逻辑xxxxxx心跳包就是及时检测对方是否连接,检查断线的一种机制。心跳包协议数据内容完全是开发者自己掌控(比如client和server可以同步数据)。
题主需要依次理解以下异常情况对正在相互进行TCP通信的两个应用程序的影响,就明白为什么在应用层依然需要维持定时心跳协议。1 对端程序close socket2 本机程序close socket3 对端主机正常关机4 对端进程被系统kill5 对端主机意外崩溃关机,比如电源停电6 对端主机重启7 本机与对端网线突然被拔掉8 本机与对端中间路由器崩溃9 对端进程陷入死锁/阻塞等故障或者bug10 对端主机负载过高 cpu使用率100%如果开启KEEPALIVE,以上这些可能又会怎么样?
游戏服务器常常有心跳包的设计。我们的心跳包就是为了防止Socket断开连接,或是TCP的连接断开吗?答案是否定的,TCP连接的通道是个虚拟的,连接的维持靠的是两端TCP软件对连接状态的维护。TCP 连接自身有维护连接的机制,说白了就是自身有长时间没有数据包情况下的判断连接是否还存在的检测,清除死连接,即使在没有数据来往的时候,TCP也就可以(在启动TCP这个功能的前提下)自动发包检测是否连接正常,这个不需要我们处理。服务端设计心跳包的目的:探知对端应用是否存活,服务端客户端都可以发心跳包,一般都是客户端发送心跳包,服务端用于判断客户端是否在线,从而对服务端内存缓存数据进行清理(玩家下线等);问题在于,通过TCP四次握手断开的设定,我们也是可以通过Socket的read方法来判断TCP连接是否断开,从而做出相应的清理内存动作,那么为什么我们还需要使用客户端发送心跳包来判断呢?第一种判断客户端是否在线策略:直接监控TCP传输协议的返回值,通过返回值处理应用层的存活判断比如在C++当中使用poll的IO复用方法时:if(fds[i].revents & POLLERR) if(fds[i].events & POLLDHUP)通过上述判断可以探知TCP连接的正确性从而在服务器也关闭对应的连接close() on the file descriptor will release resources that are still being reserved on behalf of the socket. 此时调用close()函数才会释放相关的资源。比如说进行如下处理:/*如果客户端关闭连接,则服务器也关闭对应的连接,并将用户总数减1*/
users[fds[i].fd] = users[fds[user_counter].fd];
close(fds[i].fd);
fds[i] = fds[user_counter];
user_counter--;
printf("a client left\n");
又比如在Java中:在Java的阻塞编程中:通过 ServerSocket ss = new ServerSocket(10021);
Socket so = ss.accept();
// 获取相关流对象
InputStream in = so.getInputStream();
byte[] bytes = new byte[1024];
int num = in.read(bytes);
if(num == -1)//表明读到了流的末尾,事实也就是client端断开了连接,比如调用close()
so.close();
在Java的非阻塞编程当中:通过SelectionKey key = selector.register(socketChannel,ops,handle);
SocketChannel socketChanel = (SocketChannel)key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int num = socketChannel.read(buffer);
if(num == -1)
key.channel().close();
上述连接处理方式,返回-1也好,收到POLLERR POLLDHUP也好,都是收到了客户端的fin或者rst之后的反应,所以根据四次分手原则,我们调用close方法,发送fin给客户端。上面这种策略通过TCP协议的返回值来得知客户端TCP断开,从而得知客户端掉线。当前提是如果提前根据ip或者mac做了记录,所以可以在服务器端收到TCP连接中断的消息后,调用close,并且通过socket得到玩家socket数据(具体如IP地址),从而获得user信息从而清除数据。那么这种方式有什么不完美呢?或者说有什么缺陷呢?主要原因就是TCP的断开可能有时候是不能瞬时探知的,甚至是不能探知的,也可能有很长时间的延迟,如果前端没有正常的断开TCP连接,四次握手没有发起,服务端无从得知客户端的掉线,那么就要依靠开启TCP的keep alive机制,but TCP协议的keep alive机制是不建议开启的,即使开启了默认的时间间隔是2h,泪奔!如果要服务端维持一个2h的死链接,那是可怕的,如果我们调整了时间间隔,也是有problem的,因为TCP本身就不建议TCP层的心跳检测,因为这可能导致一个完好的TCP连接在中间网络中断的情况下重启⊙▂⊙。下面有必要先介绍下TCP的keep-alive机制。关于TCP自己的keep-alive机制,参看TCP/IP详解当中有如下介绍:使用keeplive机制,可以检测到网络异常。一、什么是keepalive定时器?在一个空闲的(idle)TCP连接上,没有任何的数据流,许多TCP/IP的初学者都对此感到惊奇。也就是说,如果TCP连接两端没有任何一个进程在向对方发送数据,那么在这两个TCP模块之间没有任何的数据交换。你可能在其它的网络协议中发现有轮询(polling),但在TCP中它不存在。言外之意就是我们只要启动一个客户端进程,同服务器建立了TCP连接,不管你离开几小时,几天,几星期或是几个月,连接依旧存在。中间的路由器可能崩溃或者重启,电话线可能go down或者back up,只要连接两端的主机没有重启,连接依旧保持建立。这就可以认为不管是客户端的还是服务器端的应用程序都没有应用程序级(application-level)的定时器来探测连接的不活动状态(inactivity),从而引起任何一个应用程序的终止。然而有的时候,服务器需要知道客户端主机是否已崩溃并且关闭,或者崩溃但重启。许多实现提供了存活定时器来完成这个任务。存活定时器是一个包含争议的特征。许多人认为,即使需要这个特征,这种对对方的轮询也应该由应用程序来完成,而不是由TCP中实现。此外,如果两个终端系统之间的某个中间网络上有连接的暂时中断,那么存活选项(option)就能够引起两个进程间一个良好连接的终止。例如,如果正好在某个中间路由器崩溃、重启的时候发送存活探测,TCP就将会认为客户端主机已经崩溃,但事实并非如此。存活(keepalive)并不是TCP规范的一部分。在Host Requirements RFC罗列有不使用它的三个理由:(1)在短暂的故障期间,它们可能引起一个良好连接(good connection)被释放(dropped),(2)它们消费了不必要的宽带,(3)在以数据包计费的互联网上它们(额外)花费金钱。然而,在许多的实现中提供了存活定时器。一些服务器应用程序可能代表客户端占用资源,它们需要知道客户端主机是否崩溃。存活定时器可以为这些应用程序提供探测服务。Telnet服务器和Rlogin服务器的许多版本都默认提供存活选项。个人计算机用户使用TCP/IP协议通过Telnet登录一台主机,这是能够说明需要使用存活定时器的一个常用例子。如果某个用户在使用结束时只是关掉了电源,而没有注销(log off),那么他就留下了一个半打开(half-open)的连接。在图18.16,我们看到如何在一个半打开连接上通过发送数据,得到一个复位(reset)返回,但那是在客户端,是由客户端发送的数据。如果客户端消失,留给了服务器端半打开的连接,并且服务器又在等待客户端的数据,那么等待将永远持续下去。存活特征的目的就是在服务器端检测这种半打开连接。二、keepalive如何工作?在此描述中,我们称使用存活选项的那一段为服务器,另一端为客户端。也可以在客户端设置该选项,且没有不允许这样做的理由,但通常设置在服务器。如果连接两端都需要探测对方是否消失,那么就可以在两端同时设置(比如NFS)。若在一个给定连接上,两小时之内无任何活动,服务器便向客户端发送一个探测段。(我们将在下面的例子中看到探测段的样子。)客户端主机必须是下列四种状态之一:1) 客户端主机依旧活跃(up)运行,并且从服务器可到达。从客户端TCP的正常响应,服务器知道对方仍然活跃。服务器的TCP为接下来的两小时复位存活定时器,如果在这两个小时到期之前,连接上发生应用程序的通信,则定时器重新为往下的两小时复位,并且接着交换数据。2) 客户端已经崩溃,或者已经关闭(down),或者正在重启过程中。在这两种情况下,它的TCP都不会响应。服务器没有收到对其发出探测的响应,并且在75秒之后超时。服务器将总共发送10个这样的探测,每个探测75秒。如果没有收到一个响应,它就认为客户端主机已经关闭并终止连接。3) 客户端曾经崩溃,但已经重启。这种情况下,服务器将会收到对其存活探测的响应,但该响应是一个复位,从而引起服务器对连接的终止。4) 客户端主机活跃运行,但从服务器不可到达。这与状态2类似,因为TCP无法区别它们两个。它所能表明的仅是未收到对其探测的回复。服务器不必担心客户端主机被关闭然后重启的情况(这里指的是操作员执行的正常关闭,而不是主机的崩溃)。当系统被操作员关闭时,所有的应用程序进程(也就是客户端进程)都将被终止,客户端TCP会在连接上发送一个FIN。收到这个FIN后,服务器TCP向服务器进程报告一个文件结束,以允许服务器检测这种状态。在第一种状态下,服务器应用程序不知道存活探测是否发生。凡事都是由TCP层处理的,存活探测对应用程序透明,直到后面2,3,4三种状态发生。在这三种状态下,通过服务器的TCP,返回给服务器应用程序错误信息。(通常服务器向网络发出一个读请求,等待客户端的数据。如果存活特征返回一个错误信息,则将该信息作为读操作的返回值返回给服务器。)在状态2,错误信息类似于“连接超时”。状态3则为“连接被对方复位”。第四种状态看起来像连接超时,或者根据是否收到与该连接相关的ICMP错误信息,而可能返回其它的错误信息。具体来说:在TCP协议的机制里面,本身的心跳包机制,也就是TCP协议中的SO_KEEPALIVE,系统默认是设置2小时的心跳频率。需要用要用setsockopt将SOL_SOCKET.SO_KEEPALIVE设置为1才是打开,并且可以设置三个参数tcp_keepalive_time/tcp_keepalive_probes/tcp_keepalive_intvl,分别表示连接闲置多久开始发keepalive的ACK包、发几个ACK包不回复才当对方死了、两个ACK包之间间隔多长。TCP协议会向对方发一个带有ACK标志的空数据包(KeepAlive探针),对方在收到ACK包以后,如果连接一切正常,应该回复一个ACK;如果连接出现错误了(例如对方重启了,连接状态丢失),则应当回复一个RST;如果对方没有回复,服务器每隔多少时间再发ACK,如果连续多个包都被无视了,说明连接被断开了。“心跳检测包”是属于TCP协议底层的检测机制,上位机软件只是解析显示网口的有用数据包,收到心跳包报文属于TCP协议层的数据,一般软件不会将它直接在应用层显示出来,所以看不到。以太网中的“心跳包”可以通过“以太网抓包软件”分析TCP/IP协议层的数据流看到。报文名称”TCP Keep-Alive”。一些比较可靠的以太网转串口模块,都有心跳包的检测,比如致远电子的ZNE-100TL模块,配置“心跳包检测”间隔时间设为“10”秒,使用一款”wireshark”的抓包软件来实际查看下TCP/IP协议层“心跳包”数据。看了上面的内容,使用TCP自己的keep-alive机制,也是可以实现连接维持,通过TCP传输层的心跳包探知两端TCP连接的正确性,从而得知应用层的情况(TCP在,应用一定在,TCP不在了,应用一定不在了),但是这不是最优选择!那么既然有TCP的心跳机制,我们为什么还要在应用层实现自己的心跳检测机制呢?评论中
所说:tcpip详解卷1有网络异常中断的3种情况,比如os回收端口的时候发送的rst包,如果os挂了就不会发这个消息了。 另外如果网络异常,有可能收到路由器返回的icmp主机不可达消息从而关闭连接。 keepalive之所以不靠谱,是因为需要从socket error获知连接断开,而且是被动断开。 而应用层自己实现的heartbeat可以自主检测超时,更方便修改超时时间和断开前处理。以及@李乐所说: keepalive设计初衷清除和回收死亡时间长的连接,不适合实时性高的场合,而且它会先要求连接一定时间内没有活动,周期长,这样其实已经断开很长一段时间,没有及时性。而且keepalive好像还不能主动通知应用层,需要主动调用api去检测异常。二:使用自己应用层的心跳包,上述方法用于正常情况下的TCP连接维护,
场景举例如下:在游戏服务器当中,内存中维护着众多玩家的在线数据,以方便调用,比如玩家的英雄队伍信息,玩家的世界位置信息,在玩家下线的时候,服务器必须知道并且清除掉数据(不然就会出现一个已经下线的玩家出现在世界上),在上述举例中,在服务器端收到TCP连接中断的消息后,调用close,期间可以通过socket得到玩家socket数据,从而获得user信息(如果提前根据ip或者mac做了记录)从而清除数据。
但是如果不是正常的玩家下线,TCP的四次握手没有成功,比如网络直接中断,client端的TCP协议的fin包没有发出去,服务端就不能及时探知玩家下线,如果依赖上面讲的TCP自己的keep alive探测机制,时间较长,做不到及时处理下线玩家,并且性能不佳,因为TCP/IP的设计者本身就不支持TCP的心跳,因为这可能因为中间网络的短暂中断导致两端良好的TCP连接断开。所以一般不启用TCP的心跳机制,那我们怎么处理这些异常下线呢?如果不处理,服务端将一直维护着TCP死连接,导致网络资源(一台服务器可以支持的TCP连接有限)和内存资源(内存中可能维护着该玩家的数据)的占用,所以就要用到应用层的心跳包了下面解释下应用层的心跳包:心跳包,通常是客户端每隔一小段时间向服务器发送的一个数据包,通知服务器自己仍然在线,服务器与客户端之间每隔一段时间 进行一次交互,来判断这个链接是否有效,并传输一些可能有必要的数据。通常是在建立了一个TCP的socket连接后无法保证这个连接是否持续有效,这时候两边应用会通过定时发送心跳包来保证连接是有效的。因按照一定的时间间隔发送,类似于心跳,所以叫做心跳包。事实上为了保持长连接(长连接指的是建立一次TCP连接之后,就认为连接有效,利用这个连接去不断传输数据,不断开TCP连接),至于包的内容,是没有特别规定的,不过一般都是很小的包,或者只是包含包头的一个空包。那么心跳包的意义就在于方便的在服务端管理客户端的在线情况,并且可以防止TCP的死连接问题,避免出现长时间不在线的死链接仍然出现在服务端的管理任务中。再举下面一个例子说明下为什么TCP自身的四次握手断开机制不能完全保证应用程序探知连接断开从而避免死连接。(1)做一个游戏客户端和服务器端的测试,手动强制关闭客户端进程,然后查看服务器的情况,结果往往是,服务器收到了客户端关闭的事件。其实, 这样会忽略一个问题,没有触发异常中断事件,比如网络中断。(2)手动关闭客户端进程,事实上并不能测试出想要的结果,因为进程是在应用层的,所以,这种测试方法不能保证网络驱动层也不发送数据报文给服务器。经过测试会发现,当应用层强制结束进程时,对于TCP连接,驱动层会发送reset数据包!而服务器收到这个数据包就可以正常关闭了!(3)那么,如果网络异常甚至直接拔掉网线呢,服务器收不到这个数据包,就会导致死连接存在!(4)所以,心跳包是必要的,或者使用TCP协议本身的Keep-alive来设置(但是keep-alive不够好) 我们不能误解TCP连接如同一条绳子,一方断开了,另外一方必然会知道的。事实上TCP连接,这个“面向连接”的物理连接并不存在,它只是抽象出来的概念,是一个虚拟的连接,对于物理层,对于网线、光纤而言,不存在连接不连接的概念,因为,对它们而言,无非就是一些电流脉冲而已,具体就是一个一个的IP报文。 TCP的连接,不过是通过ACK、SEQ这些机制来模拟实现的,具体比如差错重传,拥塞控制那么心跳包的检测发送处理对服务器资源的耗费怎么判断?这个要看心跳包发送的频率,我们可以自行设置另外这里有个例程,模拟了socket心跳包的C语言实现:
补充一下,也要考虑使用多层代理的情况,在处于不活动时,连接池出于效率会释放掉当前不活动链接,腾出资源给新的请求者。
兄弟,你理解错拉。所谓的长连接,就是你建立Tcp连接的时候,如果你长时间没有数据往来的话,会被运营商网络主动断掉的。举个通俗点的例子来说,现在手机上的聊天APP,为什么会及时收到消息呢,就是因为保持了长连接,而长连接就是每隔一定时间发一个数据包,俗称心跳包,告诉运营商服务器我还在线上,不要把我断掉。为什么运营商服务器会断掉长时间没有数据往来的连接呢,这很简单,因为用户太多,要为每个用户都保持一个连接是不太现实的,所以就会检测长时间无数据往来的连接。而手机聊天App,为了不让被断掉,所以要每隔一段时间(这个时间要小于服务器检测间隔时间),主动发送数据,也就是心跳报,告诉它我还在线上,不要把我断掉。这个心跳报没什么数据,仅仅是告诉它为了让它保持这个连接不被断开。
应用层的心跳和tcp自带的心跳不是一回事tcp自带的参数keepalive是检测死连接的具体:设置keepalive后tcp会在空闲时给对方发送数据如果对方回复ack,证明连接正常如果对方回复rst,说明连接退出如果对方回复fin,说明连接崩溃如果对方未回应,继续发送直到超时。应用层心跳keepalive是自己设定的,比如http是短连接,维持连接就定时发送心跳包
已有帐号?
无法登录?
社交帐号登录

我要回帖

更多关于 服务器 客户端 通讯 的文章

 

随机推荐