存档

2010年12月 的存档

libofetion demo以及纯命令行飞信

十二月 20th, {2010 27 条评论 25,799 人阅读过  

之前一直有用户要求写一个libofetion的demo,再加上很多用户对于纯命令行版本飞信的强烈需求,于是我昨天简单地写了一个demo,把libofetion的API也做了一些修改,使它用起来更像是一个lib,不过对于第三方开发的话还是有很多很难理解的地方,因为最初并没有想把它当做一个lib来发布。到现在我对飞信的开发又要暂时先告一段落了,周末都在openfetion和娱乐中度过的,实验室项目和论文又要开始提上日程了,OK,先把code列出来,再做下简单地说明

/***************************************************************************
 *   Copyright (C) 2010 by lwp                                             *
 *   levin108@gmail.com                                                    *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program; if not, write to the                         *
 *   Free Software Foundation, Inc.,                                       *
 *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
 ***************************************************************************/
 
#include <openfetion.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
#define BUFLEN 1024
 
int   password_inputed = 0;
int   mobileno_inputed = 0;
int   tono_inputed = 0;
int   message_inputed = 0;
User *user;
pthread_t th;
 
static void usage(char *argv[]);
 
int fx_login(const char *mobileno, const char *password)
{
	Config           *config;
	FetionConnection *tcp;
	FetionSip        *sip;
	char             *res;
	char             *nonce;
	char             *key;
	char             *aeskey;
	char             *response;
	int               local_group_count;
	int               local_buddy_count;
	int               group_count;
	int               buddy_count;
	int               ret;
 
	/* construct a user object */
 	user = fetion_user_new(mobileno, password);
	/* construct a config object */
	config = fetion_config_new();
	/* attach config to user */
	fetion_user_set_config(user, config);
 
	/* start ssi authencation,result string needs to be freed after use */
	res = ssi_auth_action(user);
	/* parse the ssi authencation result,if success,user's sipuri and userid
	 * are stored in user object,orelse user->loginStatus was marked failed */
	parse_ssi_auth_response(res, user);
	free(res);
 
	/* whether needs to input a confirm code,or login failed
	 * for other reason like password error */
	if(USER_AUTH_NEED_CONFIRM(user) || USER_AUTH_ERROR(user)) {
		debug_error("authencation failed");
		return 1;
	}
 
	/* initialize configuration for current user */
	if(fetion_user_init_config(user) == -1) {
		debug_error("initialize configuration");
		return 1;
	}
 
	if(fetion_config_download_configuration(user) == -1) {
		debug_error("download configuration");
		return 1;
	}
 
	/* set user's login state to be hidden */
	fetion_user_set_st(user, P_HIDDEN);
 
	/* load user information and contact list information from local host */
	fetion_user_load(user);
	fetion_contact_load(user, &local_group_count, &local_buddy_count);
 
	/* construct a tcp object and connect to the sipc proxy server */
	tcp = tcp_connection_new();
	if((ret = tcp_connection_connect(tcp, config->sipcProxyIP, config->sipcProxyPort)) == -1) {
		debug_error("connect sipc server %s:%d\n", config->sipcProxyIP, config->sipcProxyPort);
		return 1;
	}
 
	/* construct a sip object with the tcp object and attach it to user object */
	sip = fetion_sip_new(tcp, user->sId);
	fetion_user_set_sip(user, sip);
 
	/* register to sipc server */
	if(!(res = sipc_reg_action(user))) {
		debug_error("register to sipc server");
		return 1;
	}
 
	parse_sipc_reg_response(res, &nonce, &key);
	free(res);
	aeskey = generate_aes_key();
 
	response = generate_response(nonce, user->userId, user->password, key, aeskey);
	free(nonce);
	free(key);
	free(aeskey);
 
	/* sipc authencation,you can printf res to see what you received */
	if(!(res = sipc_aut_action(user, response))) {
		debug_error("sipc authencation");
		return 1;
	}
 
	if(parse_sipc_auth_response(res, user, &group_count, &buddy_count) == -1) {
		debug_error("authencation failed");
		return 1;
	}
 
	free(res);
	free(response);
 
	if(USER_AUTH_ERROR(user) || USER_AUTH_NEED_CONFIRM(user)) {
		debug_error("login failed");
		return 1;
	}
 
	/* save the user information and contact list information back to the local database */
	fetion_user_save(user);
	fetion_contact_save(user);
 
	/* these... fuck the fetion protocol */
	struct timeval tv;
	tv.tv_sec = 1;
	tv.tv_usec = 0;
	char buf[1024];
	if(setsockopt(user->sip->tcp->socketfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) == -1) {
		debug_error("settimeout");
		return 1;
	}
	tcp_connection_recv(user->sip->tcp, buf, sizeof(buf));
 
	return 0;
}
 
int send_message(const char *mobileno, const char *receiveno, const char *message)
{
	Conversation *conv;
	Contact      *contact;
	Contact      *contact_cur;
	Contact      *target_contact = NULL;
	int           daycount;
	int           monthcount;
 
	/* send this message to yourself */
	if(*receiveno == '\0' || strcmp(receiveno, mobileno) == 0) {
		/* construct a conversation object with the sipuri to set NULL
		 * to send a message to yourself  */
		conv = fetion_conversation_new(user, NULL, NULL);
		if(fetion_conversation_send_sms_to_myself_with_reply(conv, message) == -1) {
			debug_error("send message \"%s\" to %s", message, user->mobileno);
			return 1;
		}
	}else{
		/* get the contact detail information by mobile number,
		 * note that the result doesn't contain sipuri */
		contact = fetion_contact_get_contact_info_by_no(user, receiveno, MOBILE_NO);
		if(!contact) {
			debug_error("get contact information of %s", receiveno);
			return 1;
		}
 
		/* find the sipuri of the target user */
		foreach_contactlist(user->contactList, contact_cur) {
			if(strcmp(contact_cur->userId, contact->userId) == 0) {
				target_contact = contact_cur;
				break;
			}
		}
 
		if(!target_contact) {
			debug_error("sorry,maybe %s isn't in your contact list");
			return 1;
		}
 
		/* do what the function name says */
		conv = fetion_conversation_new(user, target_contact->sipuri, NULL);
		if(fetion_conversation_send_sms_to_phone_with_reply(conv, message, &daycount, &monthcount) == -1) {
			debug_error("send sms to %s", receiveno);
			return 1;
		}else{
			debug_info("successfully send sms to %s\nyou have sent %d messages today, %d messages this monthcount",
					receiveno, daycount, monthcount);
			return 0;
		}
	}
	return 0;
}
 
int main(int argc, char *argv[])
{
	int ch;
	char mobileno[BUFLEN];
	char password[BUFLEN];
	char receiveno[BUFLEN];
	char message[BUFLEN];
 
	memset(mobileno, 0, sizeof(mobileno));
	memset(password, 0, sizeof(password));
	memset(receiveno, 0, sizeof(receiveno));
	memset(message, 0, sizeof(message));
 
	while((ch = getopt(argc, argv, "f:p:t:d:")) != -1) {
		switch(ch) {
			case 'f':
				mobileno_inputed = 1;
				strncpy(mobileno, optarg, sizeof(mobileno) - 1);	
				break;
			case 'p':
				password_inputed = 1;
				strncpy(password, optarg, sizeof(password) - 1);
				break;
			case 't':
				tono_inputed = 1;
				strncpy(receiveno, optarg, sizeof(receiveno) - 1);
				break;
			case 'd':
				message_inputed = 1;
				strncpy(message, optarg, sizeof(message) - 1);
				break;
			default:
				break;
		}
	}
 
	if(!mobileno_inputed || !password_inputed || !message_inputed) {
		usage(argv);
		return 1;
	}
 
	if(fx_login(mobileno, password))
		return 1;
 
	if(send_message(mobileno, receiveno, message))
		return 1;
 
	fetion_user_free(user);
	return 0;
 
}
 
static void usage(char *argv[])
{
	fprintf(stderr, "Usage:%s -f mobileno -p password -t receive_mobileno -d message\n", argv[0]);
}

首先需要libofetion的支持,因为用到了最新的API,所以需要从hg中clone最新版本编译安装后才可编译该程序:

hg clone https://ofetion.googlecode.com/hg/ ofetion

编译方法如下:

gcc -o cli cli.c `pkg-config --cflags --libs ofetion`

有一个地方需要说明,请大家找到/* these… fuck the fetion protocol */这个句注释,它下面的几句话的作用是这样的,飞信在用户完成身份验证之后订阅相关信息之前会推送过来一条信令:

BN 406472150 SIP-C/4.0
N: SyncUserInfoV4
I: 2
Q: 1 BN
L: 124
 
<events><event type="SyncUserInfo"><user-info>
<score value="1261" level="2" level-score="678"/>
</user-info></event></events>

用它来更新用户的积分等级之类的玩意,像这种浮云般的信令都被我直接忽略掉了,但在这种命令行模式纯粹为了发短信的情况下,不需要新建线程监听服务器推送过来的信息,但这条推送过来的信令就有可能影响其它信令的交互,而且更让人蛋疼的是并不是每次登录都会推送这条信令,有时候有,有时候没有,所以就加了在登录完成之后加了一个recv,设定了1s的超时,来处理这句信令,如果在网络情况不好的情况下,大家可以自己把超时时间设长一些,这样也就意味着在发送一条短信时在recv那里要停留几秒钟,否则就有可能导致信息发送失败。

下载地址:fetion-demo

分类: C/C++ 标签: ,

openfetion cli功能开发手记

十二月 19th, {2010 11 条评论 32,789 人阅读过  

之前一直有用户提出的命令行短信功能终于被我落实了下来,不过却让很多网友失望,因为所谓的命令行并非是纯命令行,而是需要先有openfetion GUI版本登录以作为server,cli程序通过IPC将数据交由server转发,自己并不进行与sipc server的直接数据通信,也就是必须有X图形界面的支持,当然如果要把现在的openfetion server做成不需要图形界面支持的deamon server还存在很多问题,如deamon在收到短信时该怎么处理等,另外deamon server的开发应该也需要花些时间,这个暂时未做考虑,而基于目前的飞信协议如果要发送短信则必须进行身份验证,也就是纯命令行不需要deamon server支持的飞信必须在每次发短信之前都需要登录一次,考虑到这个问题就迟迟没有开发纯命令行的版本,主要是觉得这种形式的短信发送方式存在的意义不大,不仅速度慢而且需要经过复杂的身份验证,但很多用户想用它管理server的话那也就只能这样来实现了,技术上其实问题不大,调用libofetion的api很简单就能实现,过几天抽点时间写一个看看,今天先把已经实现的CLI功能的开发过程总结一下。

首先,先看一下CLI的使用方法:

程序很简单,只加了三个个文件,分别是src/fx_server.c,src/fx_cli.c和include/fx_server.h

服务器端初始化init_server函数来初始化server,函数如下:

int init_server(FxMain *fxmain)
{
	int   fifo;
	User *user = fxmain->user;
 
	char server_fifo[128];
	snprintf(server_fifo, sizeof(server_fifo) - 1, OPENFETION_FIFO_FILE, user->mobileno);
 
	if(mkfifo(server_fifo, FIFO_FILE_MODE) == -1
			&& errno != EEXIST) {
		debug_error("create fifo %s:%s\n", server_fifo, strerror(errno));
		return -1;
	}
 
	if((fifo = open(server_fifo, O_RDONLY, 0)) == -1) {
		debug_error("open fifo %s:%s\n", server_fifo, strerror(errno));
		return -1;
	}
 
	if((idlefifo = open(server_fifo, O_WRONLY, 0)) == -1) {
		debug_error("open fifo %s:%s\n", server_fifo, strerror(errno));
		close(fifo);
		return -1;
	}
 
	return fifo;
}

这个函数对IPC进行了一些初始化,首先创建以“openfetion_fifo_登录手机号 ”命名的命名管道文件,分别打开两次,一次为只读,用于监听client发来的IPC请求,另一个为只写,这个描述符打开之后从来没有使用过,这也是UNP第二卷里面提到的小技巧,当client关闭时会关闭打开的命名管道描述符,这里server中的read函数便会返回0,从而标识client关闭,这时server便需要关闭描述符重新打开关监听,为了避免这样一种情况,server自己以只写的方式打开这个通用描述符而不写入任何数据,这样server在收不到数据时read函数便会一直阻塞。

FIFO也是基于流的通信方式,所以需要自定义消息,没有什么复杂的数据需要传输,我就简单定义了两种消息,请求消息和应答消息:

struct fifo_mesg {
	unsigned short type;
	unsigned short length;
	unsigned int  pid;
};
 
struct fifo_resp {
	unsigned short code;
	unsigned short length;
};

请求消息中的pid字段为client进程的pid,client在向server发起请求之后会打开openfetion_fifo_pid命名的FIFO等待server返回响应,而server在收到请求之后可以提取出请求消息中的pid,从来找到client用于监听的命名管道文件,将返回消息通过命名管道再反馈给client。
目前请求信令的类型只有两种:

/* 发送短信 */
#define CLI_SEND_MESSAGE    1
/* 获取用户信息 */
#define CLI_GET_INFORMATION 2

应答信令的应答码也只有两种:

/* 操作已成功 */
#define CLI_EXEC_OK   200
/* 操作失败 */
#define CLI_EXEC_FAIL 400

请求消息体中为XML格式,应答消息体中为纯文本提示消息。请求信令消息体如下:

<r><m no="15200000000" bd="hello world" p="1" /></r>

其中no表示要发送的好友手机号码,注意该好友必须在好友列表中,并且必须对你已设置公开手机号,bd为要发送的短信,p为是否用直接发送到用户手机。该消息发送到server后,server发现请求的号码与自己的号码相同时,会将消息发送至用户自己的手机中。

OK,过程就这么多吧,没有超过1K行代码,也复杂不到哪里去,接下来看看时间来不来得及写一个纯命令行的版本,有需要的同学请关注。

分类: C/C++ 标签: , , ,

linux终端中输出彩色字体(C/SHELL)

十二月 15th, {2010 3 条评论 14,703 人阅读过  

这几天在用libvlc的时候看到它在terminal里面输出彩色字体觉得挺好玩的,以为是用ncurses实现的,后来一查原来用ANSI C的转义字符就可以实现,不过好现只在linux下有效吧, windows神马的貌似不行,把项目终端凌乱的输出信息整理了一下,关键字符也都用彩色显示,看上去舒服多了,写一下用法以防止以后忘了再去查。

先把控制码列出来(从网上搜来的):

\033[0m               关闭所有属性
\033[1m                       设置高亮度
\033[4m                       下划线
\033[5m                        闪烁
\033[7m                        反显
\033[8m                        消隐
\033[30m----\33[37m 设置前景色
\033[40m----\33[47m 设置背景色
\033[nA                        光标上移n行
\033[nB                       光标下移n行
\033[nC                       光标右移n行
\033[nD                       光标左移n行
\033[y;xH                     设置光标位置
\033[2J                        清屏
\033[K                         清除从光标到行尾的内容
\033[s                          保存光标位置
\033[u                          恢复光标位置
\033[?25l                      隐藏光标
\033[?25h                     显示光标 
背景色:
40:黑 41:深红 42:绿 43:黄色 44:蓝色 45:紫色 46:深绿 47:白色
前景色:
30:黑 31:红 32:绿 33:黄 34:蓝色 35:紫色 36:深绿 37:白色

控制字符是打开某种样式,输出完成时需要再关闭样式才能使terminal恢复到原来状态,简单例子:

printf("\e[32m%s\e[0m\n", "hello world");

\033\e是一回事,使用\e会更简单一些,输出为绿色字体,如下图:

\e[32m为打开绿色前景色样式,\e[0m为关闭所有样式,如果未关闭,则所有输出字体均为绿色前景色,如下代码:

	printf("\e[32m%s\e[0m\n", "hello world");

输出效果如下:

绿色高亮代码如下:

printf("\e[32m\e[1m%s\e[0m\n", "hello world");

输出效果如下:

另外光标移动的控制码在输出类似于下载进度的信息时用得到。

在shell中也可以直接用echo输出,需要加-e选项打开转义字符解释,如输出高亮的绿色字体为:

echo -e "\e[32m\e[1mhello world\e[0m"

其它的我也没怎么看,觉得也就改变字体颜色和移动光标位置能用得到,觉得挺好玩的就写下来以后忘了查一查。

分类: Linux 标签: , ,

linux网络编程手记

十二月 12th, {2010 4 条评论 12,920 人阅读过  

做linux下的网络编程有一段时间了,中间遇到过很多问题,其中不少是因为自己对网络编程和网络协议的一些基本概念搞不清楚,趁着今天没心情干活就把自己在网络编程方面的理解和一些经验总结一下,Request For Comments。

在诸多的网络协议中接触的最多也最紧密的无疑是TCP和UDP,SCTP之前因为项目原因也研究过,不过最终由于方案修改给抛弃了,TCP年代已经很久远,在网上的资料也非常多,而且我感觉它是一种非常复杂的协议,感觉要把编好基于TCP的程序光简单地了解几个socket API是不够的,刚开始接触网络编程的时候自己确实也吃了不少苦头,后来我还专门拿时间出来阅读了一下RFC,再加上长时间的实践总算也对TCP有所了解,把自己的一些经验和教训都总结一下。

首先说一下TCP的状态转移图,这个应该是很重要的,了解TCP运行周期的各种状态才能更好地运用netstat之类的应用程序去对程序进行调试,我这里收藏了一张图,是TCP的状态图,记不清是从哪里找来的,也不知道直接版权该给谁,但这张图应该最终是出自于UNP第一卷的,那copyright就是UNP了吧。

1.TCP连接状态

连接建立的几个状态没什么可说的,TCP的三次握手众所周知,更重要的是TCP连接中止的几个状态,应该可以说是连接中止需要四次握手吧。

当Client调用close函数主动关闭socket时,连接状态被标记为FIN_WAIT_1,Server在收到FIN之后read函数会返回0,这里server知道Client已经关闭连接,回复ACK,这里client连接状态被标记为FIN_WAIT_2,接下来Server调用close函数关闭连接,这时候Server向client发送FIN,Client收到之后将状态标记为TIME_WAIT,并回复ACK。

TIME_WAIT这个状态存在的意义在于Client回复的ACK未必会被Server收到,可能在传输过程中导致包的丢失,而这里Server未收到ACK之后会重新向Client发送FIN,如果client未将状态标记为TIME_WAIT而是直接标记为CLOSED,则Server发送的FIN会直接收到RST,导致Server端的发送错误,因此Client需要保证有一个TIME_WAIT状态,而这个状态会持续两位的MSL(最大段生命周期),从而保证Server成功发送FIN并发送ACK,为了保证两个数据段传输的最大时间,因此TIME_WAIT持续的时间为两倍的MSL。

Server在收到第一个FIN之后会将状态标记为CLOSE_WAIT,此时是client主动关闭连接,这里Server也需要调用Close给Client发送FIN(如上所述),之后Server的状态标记为LAST_ACK,表示Server正在等待Client发送的最后一个ACK,当Server收到最后一个ACK便会将连接标记为CLOSED,这时连接结束。TIME_WAIT这个状态和套接字的SO_REUSEADDR选项是有关系的,这个留做后面讨论。

2.TCP连接异常情况

TCP连接异常分为很多种情况,无论是客户端程序还是服务器端程序都需要考虑周全的。

Server在连接的过程中程序崩溃或者CTRL+C中止程序,或者kill接Server进程。这时会导致Server立即发送一个FIN数据包给Client,Client如果此时正在调用recv函数,则recv函数返回0,表示服务器已关闭连接,如果Client调用send函数继续向Server发送数据,Server在收到后会回复RST,而此时send方法会触发SIGPIPE信号,表示通信管道已断开,在程序中如果对该信号不做处理则会导致程序的崩溃,一般在程序开始时会忽略此信号,则在这种情况下send函数会返回-1,表示发送失败,处理SIGPIPE的代码如下:

前几天实验室这个破项目非要加上什么流媒体的功能,简单起见使用了VLC来实现,客户端这边就得需要把相关的播放界面整合到现有的界面里面来,之前的客户端UI我都是用GTK实现的,没办法,GTK用得比较多,相对熟练一些就用GTK来做了,没想到要把VLC整到GTK里面来那么麻烦,原生的libvlc是不支持GTK的,需要加一层libvlc-gtk,从网上好不容易下载到了libvlc-gtk的源码,从哪里下的也记不清了,反正就是零散地几个文件,没有README甚至连Makefile都没有,没办法首先得先写个Makefile把它编译一下,libvlc-gtk一共有八个文件,Makefile如下:

struct sigaction sa;
sa.sa_handler = SIG_IGN;
sigaction(SIGPIPE, &sa, 0 );

另外在这种情况下select函数也会立即返回,socket描述符会被设置,而试图从该socket中recv数据,则会返回-1。

另外一种情况是Server系统崩溃或者网络直接异常或断开,这时候Server不可能再给Client发送FIN包,而Client调用send函数后会导致数据包一直重传直接超时后返回-1,而recv函数也会一直阻塞直接超时后返回-1。这种情况就很难判断是Server端进程关闭还是网络异常,这种情况一般会用TCP的KEEP ALIVE机制,每隔一定的时间向对方发送一个只有一字节数据内容的数据包,对端收到后会返回一个ACK,以此来确保连接正常,如果未收到ACK,会尝试重传,直到重试规定次数后可以将与对端的连接标记为断开,send和recv将会返回-1。KEEP ALIVE的使用方法如下:

int tcp_keep_alive(int socketfd)
{
	int keepAlive = 1;
	int keepIdle = 10;         /* 开始发送KEEP ALIVE数据包之前经历的时间 */
	int keepInterval = 10;   /* KEEP ALIVE数据包之前间隔的时间 */
	int keepCount = 10;     /* 重试的最大次数 */
 
	if(setsockopt(socketfd , SOL_SOCKET , SO_KEEPALIVE
				,(void*)&keepAlive,sizeof(keepAlive)) == -1){
		debug_info("set SO_KEEPALIVE failed\n");
		return -1;
	}
 
	if(setsockopt(socketfd , SOL_TCP , TCP_KEEPIDLE
				,(void *)&keepIdle,sizeof(keepIdle)) == -1){
		debug_info("set TCP_KEEPIDEL failed\n");
		return -1;
	}
 
	if(setsockopt(socketfd , SOL_TCP , TCP_KEEPINTVL
				,(void *)&keepInterval,sizeof(keepInterval)) == -1){
		debug_info("set TCP_KEEPINTVL failed\n");
		return -1;
	}
 
	if(setsockopt(socketfd , SOL_TCP , TCP_KEEPCNT
				,(void *)&keepCount,sizeof(keepCount)) == -1){
		debug_info("set TCP_KEEPCNT failed\n");
		return -1;
	}
	return 1;
}

上面这个函数只针对Linux,昨天有网友告知在Mac OS上TCP_KEEPIDLE ,TCP_KEEPINTVL, TCP_KEEPCNT这些宏将未定义。另外对于这些参数的设置也是需要注意的,很多系统中它们的设置并不是对单个socket描述符起作用的,而是该机器上的所有socket描述符起作用的,所以这个需要注意(这个是从UNP里面看到的)。

3.关于字节顺序
Linux的主机字节顺序是采用little-endian字节顺序,而网络字节顺序是采用big-endian字节顺序,字节顺序转换是必需的。写了一个小程序来检测字节顺序,不知道对不对,Request For Comment.

#include 
 
int main(int argc, char **argv)
{
	short s = 0x0102;
	if((*(unsigned char*)&s) == 2)
		printf("little endian\n");
	else if((*(unsigned char*)&s) == 1)
		printf("big endian\n");
	else
		printf("unknown endian\n");
 
	return 0;
}

3.关于send和recv

写过socket程序的人肯定都会知道send和recv函数并不会总是返回要求发送或读取的字节数,如:

int ret = recv(sk, buf, 2096, 0);

这句话并不总是读取到完整地2096个字节,相反地,大多数情况下都不能将buf读满,recv只能返回当前可以读取到的字节数,如果协议规定本次读取肯定会读取到N个字节,那我一般的做法会写一个这样的函数来确保读取到固定的字节数:

int buf_recv(int sock, void *buf, size_t len, int flags)
{
	int n, ret;
	if(len == 0) return 0;
	for(n=0;n!=len &&(ret = recv(sock, buf+n, len-n, flags)) != -1 &&ret; n += ret);
	return (n!=len)? -1:n;
}

关于这两个函数还有很重要的一点是应该尽可能大地一次发送或接收更多地数据,当然前提是缓冲区中有这些数据的话,原因很简单,当通信链路很好的时候数据可能会填满系统缓冲区,而recv便是从缓冲区中读取数据,这时候一次读取更多地字节就意味着可以少调用几次recv函数,而这些函数通常都是调用了系统调用,需要进行内核态和用户态上下文的切换,也就意味着多调用几次recv会带来额外的开销,之前写的一个代理服务器的程序数据传输速度一直很低,后来修改了recv和send的缓冲区大小后速率提高了近一倍。

4.关于非阻塞模式

一般应用的时候都是使用阻塞式IO,至少我在大多数情况下都用的阻塞式IO,非阻塞很少应用,但存在便我价值,我用到的非阻塞IO的情况一般是用来进行超时connect,首先将socket设为非阻塞模式,connect立即返回-1,此时已向对端发送FIN,而并未来得及收到任何ACK,于是直接返回-1,但并不代表连接失败,errno会被置为EINPROGRESS ,表示连接正在进行中,然后通过select来设置socket可写的超时时间,如果规定时间内可写,且socket并无出错,则表示连接成功,socket出错则表示连接失败,或规定时间内不可写则表示连接超时,简单地写了如下代码:

#include
#include
#include
#include
#include
#include
#include 
 
int main(int argc, char *argv[])
{
	int                sk;
	int                flags;
	int                err = 0;
	int                ret;
	socklen_t          len;
	struct sockaddr_in addr;
	fd_set             fd_write;
	struct timeval     tv;
 
	if( (sk = socket(AF_INET, SOCK_STREAM, 0)) == -1 ) {
		perror("socket");
		return 1;
	}
 
	if( (flags = fcntl(sk, F_GETFL, 0)) == -1 ){
		perror("fcntl GET flags failed");
		return 1;
	}
 
	if(fcntl(sk, F_SETFL, flags | O_NONBLOCK) == -1) {
		perror("fcntl SET flags failed");
		return 1;
	}
 
	memset(&addr, 0, sizeof(addr));
	addr.sin_family = AF_INET;
	addr.sin_addr.s_addr = inet_addr("59.64.129.169");
	addr.sin_port = htons(808);
 
	if(connect(sk, (struct sockaddr*)&addr, sizeof(addr)) == -1	) {
		if(errno != EINPROGRESS) {
			perror("connect");
			return 1;
		}
		FD_ZERO(&fd_write);
		FD_SET(sk, &fd_write);
		tv.tv_sec = 5;
		tv.tv_usec = 0;
 
		ret = select(sk + 1, (fd_set*)0, &fd_write, (fd_set*)0, &tv);
 
		if(ret > 0){
 
			if(FD_ISSET(sk, &fd_write)) {
				len = sizeof(int);
 
				if(getsockopt(sk, SOL_SOCKET, SO_ERROR, &err, &len) == 0) {
					if(err == 0) { printf("connect success\n"); return 0; }
					else { fprintf(stderr, "connect:%s\n", strerror(err)); return 1; }
				}else{
					fprintf(stderr, "getsockopt:%s\n", strerror(err));
					return 1;
				}
			}else{
				fprintf(stderr, "connect(FD_ISSET) failed\n");
				return 1;
			}
 
		}else if(ret == 0) {
			fprintf(stderr, "connect timeout\n");
			return 1;
		}else {
			fprintf(stderr, "connect(select):%s\n", strerror(errno));
			return 1;
		}
	}else{
		fprintf(stderr, "connect:%s\n", strerror(errno));
		return 1;
	}
	return 0;
}

5.关于select多路复用

select是网络编程中很常用的函数,用来进行IO多路复用,但之前我一直忽略了一个问题,当select返回时会将本次检查中不可用的描述符(如不可读或不可写)的描述符从描述符集中删除,只保留当前可用的描述符,在对多个socketfd进行利用的时候需要注意,每次循环select之前都需要在select之前用FD_SET重新设置描述符,否则之后便只能返回第一次可读的描述符了。

6.关于UDP广播

UDP广播这个也简单说一下,首先255.255.255.255这个地址是不能被路由的,只能被本物理网络的数据包接收。UDP广播之前需要给socket设置SO_BROADCAST选项:

int brodopt = 1;
setsockopt(cp_usock, SOL_SOCKET, SO_BROADCAST, &brodopt, sizeof(brodopt));

UDP广播需要注意的一点是广播接收时接收端bind的本地地址的问题,接收端必须绑定INADDR_ANY这个地址才可以接收广播包,如果是绑定的某个特定的地址则无法接收广播包。

OK,简单说这么几条,都是我编程时候遇到的经验总结,以后再遇到什么问题再接着补充。

分类: Linux 标签: , ,

抓取飞信协议数据包的bash脚本

十二月 11th, {2010 9 条评论 8,880 人阅读过  

昨晚想写个抓飞信协议包的脚本,结果刚写了一点就去看电影了,今天把它给完善了一下贴出来,也顺便可以帮助大家更好地理解飞信协议,也为了更多的人能够加入到openfetion的开发中来。

其实不是我不懂“工欲善其事必先利其器”这个道理,只是之前在写openfetion的时候我只能跑到windows下去抓包,windows里面的什么vbs之类的脚本我也不会写,就用最笨的办法用wireshark把数据包抓出来然后复制到txt中,现在在linux下可以抓openfetion的数据包,它的包格式与官方飞信相同(这句是废话,要不然也不可能实现互联互通)。

简单地说一下这个脚本,其实不用说它也很简单,就这么几行,飞信在登录的时候需要向一台sipc服务器注册,随后主要的数据包都是与这台服务器之前交换的,而这台服务器我想肯定也是分布式架构了,不同的飞信号会对应于自己的sipc服务器,而服务器的地址是通过配置文件传递给客户端的,客户端在注册之前先向nav.fetion.com.cn这个地址POST一个http请求,随后服务器会响应一个大的xml,之前版本的openfeion并没有对这些数据做本地缓存,所以在每次登录的时候都需要获取这个配置文件,所以经常会有用户反应卡在“正在下载配置文件”这一步进行不下去了,现在的方法是在本地把需要用到的字段做了缓存,再次登录的时候用缓存版本号查询服务器,如果版本号是最新则会返回一个很小的xml结构,速度当然也会比完全下载快得多。

接着说这个脚本,请求到配置文件之后便会得到飞信号对应的sipc服务器地址,之后就用tcpdump来抓取通往这个地址的包和来自这个地址的包,然后就简单做了一下解析。

#!/bin/sh
#written by @levin108
#This script is used to capture the packets of fetion client,and help to anlysis fetion protocol.
#You need to have openfetion installed in your computer,or maybe libfetion,I don't know,whatever...
 
if [ $UID -ne 0 ]; then
	echo "perminission denied,you must be root to run this script"
	exit 1
fi
 
if [ $# == 0 ]; then
	echo -n "please input the mobile number:"
	read mobileno
else
	mobileno=$1
fi
 
if [ ${#mobileno} -ne 11 ];then
	echo "wrong mobile number"
	exit 1
fi
 
protocol_version="4.0.2510"
config_uri="http://nav.fetion.com.cn/nav/getsystemconfig.aspx"
config_body="<config><user mobile-no=\"$mobileno\"/> \
			<client type=\"PC\" version=\"$protocol_version\" \
			platform=\"W5.1\"/><servers version=\"0\"/> \
			<parameters version=\"0\"/><hints version=\"0\"/></config>"
 
echo "Getting fetion sipc servr address..."
 
config_xml=`curl -d "$config_body" -A "IIC2.0/PC $protocol_version" $config_uri 2> /dev/null`
proxy_endpoint=`echo $config_xml | sed 's/.*<sipc-proxy>\([^<]*\).*/\1/'`
proxy_ip=`echo $proxy_endpoint | sed 's/:[0-9]*$//'`
proxy_port=`echo $proxy_endpoint | sed 's/.*://'`
 
echo "Sipc server address : $proxy_ip:$proxy_port"
echo "Start capturing,now start your fetion client and login."
echo
 
tcpdump -n -A -l -t -s 0 tcp and host $proxy_ip \
	and port $proxy_port or host nav.fetion.com.cn 2>/dev/null | awk '
/^IP/{
	if($NF == 0) {
		is_data_seg = 0;
	} else {
		printf("\n%s ----> %s\n", $2, $4)
		printf("-----------------------------------\n\n")
		# whether it is a data segment
		is_data_seg = 1;
		# eth/ip/tcp header length
		bytes_before_protocol = 52;
	}
}
 
!/^IP /{
	if(is_data_seg) {
		if(length($0) < bytes_before_protocol) {
			bytes_before_protocol -= length($0)
			next;
		}
		if(length($0) >= bytes_before_protocol && 
			bytes_before_protocol != 0) {
			$0 = substr($0, bytes_before_protocol);
			bytes_before_protocol = 0;
		}
		print $0
	}
}
'

分类: Linux 标签: , , ,

libvlc-gtk使用手记

十二月 5th, {2010 2 条评论 7,684 人阅读过  

前几天实验室这个破项目非要加上什么流媒体的功能,简单起见使用了VLC来实现,客户端这边就得需要把相关的播放界面整合到现有的界面里面来,之前的客户端UI我都是用GTK实现的,没办法,GTK用得比较多,相对熟练一些就用GTK来做了,没想到要把VLC整到GTK里面来那么麻烦,原生的libvlc是不支持GTK的,需要加一层libvlc-gtk,从网上好不容易下载到了libvlc-gtk的源码,从哪里下的也记不清了,反正就是零散地几个文件,没有README甚至连Makefile都没有,没办法首先得先写个Makefile把它编译一下,libvlc-gtk一共有八个文件,Makefile如下:

libvlc_objs=gtk-libvlc-media.o gtk-libvlc-instance.o gtk-libvlc-media-player.o
libvlc_cflags=`pkg-config --cflags gtk+-2.0 libvlc` -I../ -fPIC -shared
libvlc_ldflag=-fPIC -shared `pkg-config --libs gtk+-2.0 libvlc`
libvlc_so=libvlcgtk.so
 
%.o:%.c
	gcc -o $@ -c $< -g $(libvlc_cflags) 
 
$(libvlc_so):$(libvlc_objs)
	gcc $(libvlc_ldflag) -o $(libvlc_so) $(libvlc_objs)
 
install:
	install libvlcgtk.so /usr/local/lib/libvlcgtk.so
	ldconfig
 
clean:
	rm -f *.o *.so

当然这样在编译的时候还会有些问题,libvlc-gtk理所当然需要libvlc的支持,而libvlc各个版本之间的函数有一点点不一样,所以就需要进行条件编译,而libvlc-gtk把条件编译选项都写好了,用了自定义的几个版本号的宏来检测相关的版本,如果这几个宏没有定义便会在编译时报错,相关代码如下:

// Set default value for LIBVLC_VERSION_MAJOR
#ifndef LIBVLC_VERSION_MAJOR
#error "LIBVLC_VERSION_MAJOR must be defined."
#endif
 
// Set default value for LIBVLC_VERSION_MINOR
#ifndef LIBVLC_VERSION_MINOR
#error "LIBVLC_VERSION_MINOR must be defined."
#endif
 
// Set default value for LIBVLC_VERSION_REVISION
#ifndef LIBVLC_VERSION_REVISION
#error "LIBVLC_VERSION_REVISION must be defined."
#endif

检查了一下相关的函数后我自己在gtk-libvlc-include.h里面定义了这几个宏:

#define LIBVLC_VERSION_MAJOR    0
#define LIBVLC_VERSION_MINOR    9
#define LIBVLC_VERSION_REVISION 0

当然也可以通过其它的方式定义,如在Makefile里面,我这里图省事就直接在这里写死了。

libvlc-gtk的API使用起来也很简单,下面的一段代码是用来播放在5004端口接收到的UDP媒体流:

	#define MRL rtp://:5004
int player_init()
{
        GtkLibvlcInstance    *inst;
	GtkLibvlcMedia       *vlc_media;
	GtkWidget            *player;
	GError               *error = NULL;
	GtkWindow            *window;
 
	/* load the vlc instance */
	inst = gtk_libvlc_instance_new(NULL, NULL, &error);
	return_if_fail(error);
 
	/* creat a new media item */
	vlc_media = gtk_libvlc_media_new(MRL);
 
	/* creat a media player */
	player = gtk_libvlc_media_player_new(inst, &error);
	gtk_widget_set_usize(media->player, 600, 500);
	return_if_fail(error);
 
	gtk_container_add(GTK_CONTAINER(window), player);
	g_signal_connect(G_OBJECT(GTK_WIDGET(media->player)),
                              "expose_event",
                              G_CALLBACK(draw_able_expose),
                              NULL);
	/* the player widget must be realized before playing */
	gtk_widget_realize(media->player);
 
	/* add a media to the player */
	gtk_libvlc_media_player_add_media(
			GTK_LIBVLC_MEDIA_PLAYER(media->player),
			vlc_media);
 
	gtk_libvlc_media_player_play(
			GTK_LIBVLC_MEDIA_PLAYER(media->player),
			NULL, &error);
 
	g_object_unref(inst);
	g_object_unref(vlc_media);
        return 0;
}

不同版本的libvlc这之间MRL的格式也有所不同,0.23版本的MRL格式如:rtp://:5004,0.22版本的则如:rtp://@:5004

既然player被封装成一个widget,那么我们就可以用gdk的一些方法很容易地在它上面绘图,如我在播放前加上了一个logo:

g_signal_connect(G_OBJECT(GTK_WIDGET(media->player)),
						  "expose_event",
						  G_CALLBACK(draw_able_expose),
						  NULL);
 
static gboolean draw_able_expose(GtkWidget *widget,
			GdkEventExpose *e, gpointer data)
{
	GdkGC       *gc;
	GdkColormap *colormap;
	GdkPixbuf   *pixbuf;
	GdkColor     color;
	GdkDrawable *drawing;
	gint         width;
	gint         height;
 
	drawing = widget->window;
 
	pixbuf = gdk_pixbuf_new_from_file_at_size(
				"skin/mplayer.svg", 96, 96,NULL);
 
	gc=gdk_gc_new(drawing);
	colormap=gtk_widget_get_colormap(widget);
	gdk_color_parse("white",&color);
	gdk_color_alloc(colormap,&color);
	gdk_gc_set_foreground(gc,&color);
 
	gdk_drawable_get_size(drawing, &width, &height);
 
	gdk_draw_pixbuf(drawing, gc, pixbuf, 0, 0,
			(width - 96) / 2, (height - 96) / 2,
			96, 96, GDK_RGB_DITHER_NORMAL, 0, 0);
	g_object_unref(pixbuf);
}

最基本的呈现一个视频流的用法就这些了,像其它的一些API也很简单的,都是采用GTK的命名风格,即便没有注释也可以很简单地了解它们的用法,就不多说了。

分类: C/C++ 标签: ,