存档

文章标签 ‘C/C++’

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

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

这几天在用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,747 人阅读过  

做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 标签: , ,

libvlc-gtk使用手记

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

前几天实验室这个破项目非要加上什么流媒体的功能,简单起见使用了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++ 标签: ,

Openfetion近期开发手记(相关功能实现技术)

十月 18th, {2010 29 条评论 8,205 人阅读过  

离上Openfetion上一个版本发布至今过了有将近一个月的时间了,上个版本放出的时候本以为已经解决了很多bug,但发布之后才发现用户遇到的问题还是很多,软件测试还是很重要的,当然有些协议上的问题只能交由用户去测试,我没有那么多飞信号,有些问题有很难遇到的,也很难复现的,所以就在这样不断地与用户沟通中解决问题。

其实做共享软件是件很开心的事情,不管你做得好不好,都会有人支持你,这个是很重要的,因为在最初开发openfetion的时候并没有想过要把它做为一个专业的飞信客户端放出来大家一起使用,可发布之后得到了广大linux用户的支持,即使那时候的bug比现在多得多,仍然有用户乐此不彼地帮我测试,反溃问题,提供建议,当然我也乐此不彼地修改程序,希望有朝一日它能让所有的用户都能稳定地运行,这也是自由软件的优势,如果当初这做为一个商业软件发布,我想毫无疑问,收到的会是一片骂声,然后这个项目也便会匆匆截止,而现在即便很忙也会拿出一些时间来加强它,因为总会有用户支持着它,也会有开源爱好者加入进来做一些贡献。

Openfetion下一个的版本号应该是2.0了,我想2.0应该和之前的1.x版本有所区别,功能上这个不是特别重要,因为之前的版本已经有了几乎所有的基本功能,其它的一些不常用的功能我也没考虑过,因为开发那些功能是一件性价比很低的事情,现在所能想到的2.0和1.x的区别应该是让2.0更加稳定,功能再丰富如果不能稳定运行这个软件就永远成不了优秀的软件,因为我在新版本里面做了大批量的代码修改,甚至包括以前一些很不专业的编码习惯,尽可能将所有潜在的问题都消灭掉,当然也加了一些用户一直以来要求的功能,下面简单地说一下。

首先,是在之前版本中,用户反映登录速度过慢,这个我承认,是因为在之前的版本中没有加入数据的本地缓存,每次用户登录的时候都会需要重新从服务器上请求自己相关的所有数据,包括用户列表和配置文件这样庞大到几个K甚至十几个K的数据,这不可避免地会导致登录过程过慢,甚至网络状况不好的时候,会导致在获取配置文件的时候卡在那里,这些问题都降低了用户体验,解决这些问题的方法毫无疑问是加入本地用户缓存,这也是网络软件所常用的方式,之前我把一些本地配置信息和聊天记录保存在本地所用的方法是直接使用二进制写入dat文件,那种方法灵活性非常差,而且效率也很低,所有就没有对其它的动态数据做缓存,现在在新版本中引用了sqlite3,这个轻量级的数据库无疑是实现这个功能的绝佳选择,使用起来很简单,而且灵活性也很高,基本的SQL语句几乎都支持,之前只是知道有这么个东西,但一直没用过,这次试了一下,发现使用起来也非常简单,于是毫不犹豫就把它给引入了,我想加入了这样一个依赖所带来的用户体验的提高是很大的,希望不会有用户抱怨引赖关系增多。

另外,在将数据进行本地缓存之后便为另一个功能的实现提供了基础,那就是离线登录功能,和IM的离线功能一样,就是在没有网络连接的情况下登录Openfetion,可以查看好友列表,当然这些好友信息都是存储在本地数据中的,通过sqlite3从数据库中提取出来的。

聊天记录改用sqlite3存储之后提取和写入也都方便了很多,而且还添加了删除聊天记录的功能,之前用二进制数据直接写入的方式保存聊天记录所带来的不便就是不能方便地删除聊天记录,如果要删除只能先把整个聊天记录都加载到内存中,然后从中删掉要删除的信息,之后再重新写入磁盘覆盖掉原来的文件,这样效率是非常低的,而用sqlite3直接可以用一条DELETE语句删掉想要删除的信息。

同样这次也加入了本地用户列表删除功能,用户登录完后记录在本地的用户名密码数据也同样都可以删除。

另一个很重要的功能是空闲时间自动离开功能,这个功能之前一直不知道该怎么实现,纠结于当焦点不在Openfetion中时,如何获取全局的鼠标键盘动作,而即便获取到了又如何检测是否空闲,这些问题都非常麻烦,后来查看了一下pidgin的源码,才发现其实IM软件所实现的空闲时间检测功能一般都是通过调用XScreenSaver来实现的,包括之前的evaqq也是之样实现的,过程很简单,下面的几行代码便是获取空闲时间的函数,通过周期性地检测空闲时间便可以判断出IM是否需要进行离开状态。

gint idle_timesec(void)
{
 
#ifdef USE_LIBXSS
	static XScreenSaverInfo *mit_info = NULL;
	static gint has_extension = -1;
	gint event_base, error_base;
 
	if (has_extension == -1)
		has_extension = XScreenSaverQueryExtension(
				GDK_DISPLAY(), &event_base, &error_base);
 
	if(has_extension){
		if (mit_info == NULL)
			mit_info = XScreenSaverAllocInfo();
 
		XScreenSaverQueryInfo(GDK_DISPLAY(), GDK_ROOT_WINDOW(), mit_info);
		return (mit_info->idle)/1000;
	}
#endif
	return 0;
}

这需要XScreenSaver的支持才行,于是加入了条件编译,在ubuntu或者debian中可以通过下面的命令安装XScreenSaver开发包:

sudo apt-get install libxss-dev

另外一个很重要的功能是断线自动离开功能,这个用tcp的相关特性来检测链路状态灵敏性太低,参考了一下pidgin和empathy,它们用的方法都是调用NetworkManager的相关API来实现的,NetworkManager是基于dbus的,之前在slackware13.1上因为安装NetworkManager失败,而导致之前同学写的基于nm和dbus的程序在我这里不能跑,从而对这个一直留有某种恐惧感,当然现在也意识到要让程序在网络状态改变的时候即刻感知到,最佳方法还是使用libnm,下面的函数便是初始化libnm的函数,它为网络状态改变的事件注册了回调函数nm_state_change()

void fx_conn_init(FxMain *fxmain)
{
#ifdef USE_NETWORKMANAGER
	GError *error = NULL;
        DBusGConnection *nm_conn = NULL;
        DBusGProxy *nm_proxy = NULL;
 
	nm_conn = dbus_g_bus_get(DBUS_BUS_SYSTEM, &error);
	if (!nm_conn) {
		debug_error("Error connecting to DBus System service: %s.\n", error->message);
	} else {
		nm_proxy = dbus_g_proxy_new_for_name(nm_conn,
		                                     NM_DBUS_SERVICE,
		                                     NM_DBUS_PATH,
		                                     NM_DBUS_INTERFACE);
		dbus_g_proxy_add_signal(nm_proxy, "StateChange", G_TYPE_UINT, G_TYPE_INVALID);
		dbus_g_proxy_connect_signal(nm_proxy, "StateChange",
		                        G_CALLBACK(nm_state_change), fxmain, NULL);
	}
#endif
}

下面的代码是回调函数的代码,在它里面能过检测各种网络状态而做出相应的动作:

static void
nm_state_change(DBusGProxy *proxy, NMState state, gpointer data)
{
	switch(state)
	{
		case NM_STATE_CONNECTED:
			debug_info("network is connected");
			break;
		case NM_STATE_ASLEEP:
			debug_info("network is sleeping...");
			break;
		case NM_STATE_CONNECTING:
			debug_info("network is connecting...");
			break;
		case NM_STATE_DISCONNECTED:
			debug_info("network is disconnected");
			break;
		case NM_STATE_UNKNOWN:
			debug_info("unknown network state");
		default:
			break;
	}
}

ubuntu或debian中NetworkManager开发包的安装方法如下:

sudo apt-get install libnm-glib-deb

要说的是,XScreenSaver和NetworkManager的使用都是可选项,为了避免不愿意引入这些库的用户抱怨,可以在configure的时候用–disable-screensaver和–disable-nm将其禁用。

另外也解决几个崩溃的bug,比如添加好友时崩溃,这个纯粹是我编码过程中出现的失误,还有群发短信时崩溃的问题,这个也是我编码的失误,都已经修改过来了,有时候收到的信息会显示发送失败,这个也修改好了,一个比较重要的bug是多人同时聊天在窗口切换的时候可能会崩溃的问题,这个问题现在也已经解决了。

有时候程序会出现 ”Program received signal SIGPIPE, Broken pipe.“这样的错误,这个信号一般是在服务器端主动关闭连接时客户端会收到的来自操作系统的信号,理论上服务器端主动关闭连接这个可能性不大,但它有时候确实会出现,之前没有对这个信号进行处理,这次把这个信号直接忽略了,然后在send和recv的时候就会返回-1,通过返回值就可以检测连接状态。

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

还有用户提到在登录时登录按钮状态不变,这个之前没怎么在意,这次也修改过来了,不过没有加取消登录的功能,这个也考虑过,不过没想到实现的方法,因为登录线程用的是gthread库,而它没有像pthread一样提供取消线程的方法,一时间也不知道该怎么去实现,这个先暂时一放,希望有了解这个的朋友可以提供些帮助,现在的实现方法是像官方飞信一样在点击登录后改变登录界面,自己还用GIMP做了个登录正在进行的gif,就是让Openfetion的图标一直在转,哈哈,很有成就感,上两个登录界面的图吧。

image image

图片里面没有打码,这个飞信号是我测试用的小号,无所谓了,反正也不用。

这次新版本想多测试一段时间再发,肯定还存在问题,尽可能在发布之前能解决更多的问题,能让2.0正式版更稳定,能让用户更满意,同时也欢迎大家到svn上co最新的版本试用,并帮忙测试,如果你有问题,请向我反溃,这样我才能帮你解决问题。svn :

svn checkout http://ofetion.googlecode.com/svn/trunk/ ofetion-read-only

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

linux0.11里面发现的好玩的链表指针迭代方法

八月 30th, {2010 2 条评论 7,292 人阅读过  

linux的内存管理一直是让我最困惑的,2.6版本内核里面各种复杂的机制搞得我云里雾里,甚至搞不明白内存管理最基本的原理是什么,于是翻阅早期版本的内核0.11,最底层的内存分段和分页我都已经在我的plinux上实践过了,读到malloc的时候发现了linus用了一种很好玩的指针迭代方法,至少我以前没见过,见识短浅,大家莫笑。

linus把一页内存分成一个个具有相同大小的内存块,然后又将这些个内存块组成一个链表,而这些内存块都是将来要分配出来的空闲内存,它们内部并不包含有任何对象,要组成链表就必然要求有指针,linus把每块内存的前4个字节拿出来作为指针,指向下一块内存的首地址:

char  *cp;
/* 在这里获取一页空闲内存 */
(void*)cp = get_free_page();
 
for(i = PAGE_SIZE/bdir->size; i > 1; i --){
/* 我觉得这里很有意思,把一个字符指针强制转换成指向字符指针的指针,
然后再对其解引用赋值,相当于把cp开始的前4个字节作为一个指针并对它进行定向 */
    *((char**)cp) = cp + bdir->size;
    cp += bdir->size;
}

然后在指针迭代的时候用了下面这种方法,不复杂,只是觉得挺有意思的:

void *retval = (void*)freeptr;
freeptr = *((void**)retval);

这样freeptr就指向了下一块空闲内存。

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

C语言获取汉字拼音首字母

七月 14th, {2010 6 条评论 13,217 人阅读过  

很多网友提意见让我给小飞信加上好友搜索功能,这几天太忙,抽时间把其它的功能加了一下,现在就剩下这个功能没做好了,想做成按汉字首字母搜索的那种,于是查了查资料写了个把汉字转换成首字母的程序,贴一下有需要的可以拿去用,也顺便给自己做个备份。

下面是程序,用了glib里面的一个函数g_convert,用iconv也可以,g_convert更方便一些就直接拿来用了。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <glib.h>
 
gchar getpychar(guchar uword0 , guchar uword1)
{
	gchar pychar;
 
	int i1 = (short)(uword0 - '\0');
	int i2 = (short)(uword1 - '\0');
 
	int tmp = i1 * 256 + i2;
 
	if(tmp >= 45217 && tmp <= 45252) pychar = 'A'; 
	else if(tmp >= 45253 && tmp <= 45760) pychar = 'B'; 
	else if(tmp >= 45761 && tmp <= 46317) pychar = 'C'; 
	else if(tmp >= 46318 && tmp <= 46825) pychar = 'D'; 
	else if(tmp >= 46826 && tmp <= 47009) pychar = 'E'; 
	else if(tmp >= 47010 && tmp <= 47296) pychar = 'F'; 
	else if(tmp >= 47297 && tmp <= 47613) pychar = 'G'; 
	else if(tmp >= 47614 && tmp <= 48118) pychar = 'H'; 
	else if(tmp >= 48119 && tmp <= 49061) pychar = 'J'; 
	else if(tmp >= 49062 && tmp <= 49323) pychar = 'K'; 
	else if(tmp >= 49324 && tmp <= 49895) pychar = 'L'; 
	else if(tmp >= 49896 && tmp <= 50370) pychar = 'M'; 
	else if(tmp >= 50371 && tmp <= 50613) pychar = 'N'; 
	else if(tmp >= 50614 && tmp <= 50621) pychar = 'O'; 
	else if(tmp >= 50622 && tmp <= 50905) pychar = 'P'; 
	else if(tmp >= 50906 && tmp <= 51386) pychar = 'Q'; 
	else if(tmp >= 51387 && tmp <= 51445) pychar = 'R'; 
	else if(tmp >= 51446 && tmp <= 52217) pychar = 'S'; 
	else if(tmp >= 52218 && tmp <= 52697) pychar = 'T'; 
	else if(tmp >= 52698 && tmp <= 52979) pychar = 'W'; 
	else if(tmp >= 52980 && tmp <= 53640) pychar = 'X'; 
	else if(tmp >= 53689 && tmp <= 54480) pychar = 'Y'; 
	else if(tmp >= 54481 && tmp <= 55289) pychar = 'Z'; 
	else pychar = ' ';
 
	return pychar;
}
 
gchar *getpystring(const gchar *in)
{
	gsize inlen , olen , i , j = 0;
	gchar *gword = g_convert(in , strlen(in)
			, "gb2312" , "utf8" , &inlen , &olen , NULL);
 
	guchar *uword = (guchar*)gword;
	gchar *out = (gchar*)malloc(olen);
 
	memset(out , 0 , olen);
 
	for(i = 0 ; i < olen ; i++){
		if(uword[i] >= 0xa1){
			if(uword[i] != 0xa3){
				out[j++] = getpychar(uword[i] , uword[i + 1]);
				i ++;
			}
		}else{
			out[j++] = (gchar)uword[i];
		}
	}
 
	return out;
 
}
 
int main(int argc , char **argv)
{
	printf("%s\n" , getpystring("linux是一个出色的操作系统"));
	return 0;
 
}

程序输出:linuxSYGCSDCZXT

分类: C/C++ 标签:

DNS解析协议的C语言简单实现

五月 11th, {2010 9 条评论 22,693 人阅读过  

仔细看了看DNS协议的相关东西,其实实际编程的时候根本用不到DNS细节的东西,要获取域名的时候经终端下用host或者nslookup指令就可以,在c里面使用gethostbyname或者getaddrinfo都能很轻松得将dns域名解析为ip地址,写这个纯粹出于个人兴趣,或者说是闲得吧。

在进行域名解析的时候,解析程序向域名服务器发起请求,域名服务器也就是在操作系统网络配置的时候写进去的那个DNS服务器地址,或者也有可能是由ISP提供的自动获取的,原理都一样,域名服务器收到请求后进行处理,首先在本地缓存中查找对应的域名,找到后将IP地址直接返回,找不到就向其它的授权服务器请求数据,又可以分为著名的递归查询和非递归查询。

递归查询就是说自始至终都由一台域名服务器进行查询,它在自己这里找不到的时候会向其它的域名服务器请求并且获取数据,然后返回给请求方。

非递归查询是指域名服务器收到请求后,如果自己有这个域名的信息就返回,如果没有就返回其它域名服务器的指针,请求方再根据这些域名服务器再发起查询。

按自己的理解瞎扯了一通,也不知道准不准确,关于DNS的相关资料网上有的是,中文的都大批大批的。

DNS服务器的原理其实没什么好说的,每天都在跟DNS打交道,但DNS的协议在实现上还是稍微有点意思的,本来想写个程序来测试一个我所了解的DNS协议,后来在写的时候还真发现一个小问题,DNS域名有时候会是一个主域名的别名,比如www.baidu.com,它就是www.a.shifen.com这个域名的别名,在DNS请求发送过去之后,response里面会有一个类型为CNAME的Answers项,里面包含了主域名的相关信息(其实也就是主域名的名称和TTL),在这个应答消息里面可能会出现多个域名消息,比如每个Answers的第一个字段就是一个域名,当然为了减少数据包的容量,DNS系统对域名进行了压缩,同一个域名只会出现一次,其它的时候再出现的话就会用一个DNS指针表示。
比如域名:www.baidu.com在数据包中的表示是 03 77 77 77 05 62 61 69 64 75 03 63 6f 6d 00

粗体的是长度,将域名中的点去掉,用长度来分隔域名,以0结束。DNS允许的长度为0-63个字节,所以一个8位的长度最高两位都为0。

而如果此处域名重复出现,信令中便会用DNS指针代替长度,指针为两个字节,16位的最位都为1,剩下的14位表示在在整个数据包中的偏移量,当程序读取到c00c的时候很容易判断它是一个指针而不是一个长度字段,于是根据c00c指向的领移量,即从数据包开始后的第12个字节,跳转过去读取出域名信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
 
#define DNS_SVR "211.68.71.4"
 
#define DNS_HOST  0x01
#define DNS_CNAME 0x05
 
int socketfd;
struct sockaddr_in dest;
 
static void 
send_dns_request(const char *dns_name);
 
static void
parse_dns_response();
 
/**
 * Generate DNS question chunk
 */
static void 
generate_question(const char *dns_name
		, unsigned char *buf , int *len);
 
/**
 * Check whether the current byte is 
 * a dns pointer or a length
 */
static int
is_pointer(int in);
 
/**
 * Parse data chunk into dns name
 * @param chunk The complete response chunk
 * @param ptr The pointer points to data
 * @param out This will be filled with dns name
 * @param len This will be filled with the length of dns name
 */
static void
parse_dns_name(unsigned char *chunk , unsigned char *ptr
		, char *out , int *len);
 
int main(int argc , char *argv[]){
 
	if(argc != 2){
		printf("Usage : %s <domain name>\n" , argv[0]);
		exit(-1);
	}
	socketfd = socket(AF_INET , SOCK_DGRAM , 0);
	if(socketfd < 0){
		perror("create socket failed");
		exit(-1);
	}
	bzero(&dest , sizeof(dest));
	dest.sin_family = AF_INET;
	dest.sin_port = htons(53);
	dest.sin_addr.s_addr = inet_addr(DNS_SVR);
 
	send_dns_request(argv[1]);
 
	parse_dns_response();
 
	return 0;
}
 
static void parse_dns_response(){
 
	unsigned char buf[1024];
	unsigned char *ptr = buf;
	struct sockaddr_in addr;
	char *src_ip;
	int n , i , flag , querys , answers;
	int type , ttl , datalen , len;
	char cname[128] , aname[128] , ip[20] , *cname_ptr;
	unsigned char netip[4];
	size_t addr_len = sizeof(struct sockaddr_in);
 
	n = recvfrom(socketfd , buf , sizeof(buf) , 0
		, (struct sockaddr*)&addr , &addr_len);
	ptr += 4; /* move ptr to Questions */
	querys = ntohs(*((unsigned short*)ptr));
	ptr += 2; /* move ptr to Answer RRs */
	answers = ntohs(*((unsigned short*)ptr));
	ptr += 6; /* move ptr to Querys */
	/* move over Querys */
	for(i= 0 ; i < querys ; i ++){
		for(;;){
			flag = (int)ptr[0];
			ptr += (flag + 1);
			if(flag == 0)
				break;
		}
		ptr += 4;
	}
	printf("-------------------------------\n");
	/* now ptr points to Answers */
	for(i = 0 ; i < answers ; i ++){
		bzero(aname , sizeof(aname));
		len = 0;
		parse_dns_name(buf , ptr , aname , &len);
		ptr += 2; /* move ptr to Type*/
		type = htons(*((unsigned short*)ptr));
		ptr += 4; /* move ptr to Time to live */
		ttl = htonl(*((unsigned int*)ptr));
		ptr += 4; /* move ptr to Data lenth */
		datalen = ntohs(*((unsigned short*)ptr));
		ptr += 2; /* move ptr to Data*/
		if(type == DNS_CNAME){
			bzero(cname , sizeof(cname));
			len = 0;
			parse_dns_name(buf , ptr , cname , &len);
			printf("%s is an alias for %s\n" , aname , cname);
			ptr += datalen;
		}
		if(type == DNS_HOST){
			bzero(ip , sizeof(ip));
			if(datalen == 4){
				memcpy(netip , ptr , datalen);
				inet_ntop(AF_INET , netip , ip , sizeof(struct sockaddr));
				printf("%s has address %s\n" , aname , ip);
				printf("\tTime to live: %d minutes , %d seconds\n"
						, ttl / 60 , ttl % 60);
			}
			ptr += datalen;
		}
 
	}
	ptr += 2;
}
 
static void
parse_dns_name(unsigned char *chunk
		, unsigned char *ptr , char *out , int *len){
	int n , alen , flag;
	char *pos = out + (*len);
 
	for(;;){
		flag = (int)ptr[0];
		if(flag == 0)
			break;
		if(is_pointer(flag)){
			n = (int)ptr[1];
			ptr = chunk + n;
			parse_dns_name(chunk , ptr , out , len);
			break;
		}else{
			ptr ++;
			memcpy(pos , ptr , flag);	
			pos += flag;
			ptr += flag;
			*len += flag;
			if((int)ptr[0] != 0){
				memcpy(pos , "." , 1);
				pos += 1;
				(*len) += 1;
			}
		}
	}
 
}
 
static int is_pointer(int in){
	return ((in & 0xc0) == 0xc0);
}
 
static void send_dns_request(const char *dns_name){
 
	unsigned char request[256];
	unsigned char *ptr = request;
	unsigned char question[128];
	int question_len;
 
 
	generate_question(dns_name , question , &question_len);
 
	*((unsigned short*)ptr) = htons(0xff00);
	ptr += 2;
	*((unsigned short*)ptr) = htons(0x0100);
	ptr += 2;
	*((unsigned short*)ptr) = htons(1);
	ptr += 2;
	*((unsigned short*)ptr) = 0;
	ptr += 2;
	*((unsigned short*)ptr) = 0;
	ptr += 2;
	*((unsigned short*)ptr) = 0;
	ptr += 2;
	memcpy(ptr , question , question_len);
	ptr += question_len;
 
	sendto(socketfd , request , question_len + 12 , 0
	   , (struct sockaddr*)&dest , sizeof(struct sockaddr));
}
 
static void
generate_question(const char *dns_name , unsigned char *buf , int *len){
	char *pos;
	unsigned char *ptr;
	int n;
 
	*len = 0;
	ptr = buf;	
	pos = (char*)dns_name; 
	for(;;){
		n = strlen(pos) - (strstr(pos , ".") ? strlen(strstr(pos , ".")) : 0);
		*ptr ++ = (unsigned char)n;
		memcpy(ptr , pos , n);
		*len += n + 1;
		ptr += n;
		if(!strstr(pos , ".")){
			*ptr = (unsigned char)0;
			ptr ++;
			*len += 1;
			break;
		}
		pos += n + 1;
	}
	*((unsigned short*)ptr) = htons(1);
	*len += 2;
	ptr += 2;
	*((unsigned short*)ptr) = htons(1);
	*len += 2;
}

分类: Protocol 标签: , ,

STUN协议的C程序

四月 26th, {2010 2 条评论 13,400 人阅读过  

写了一个小程序测试STUN协议,只可惜国内没有可用的STUN Server,UDP协议又不能穿透HTTP代理,所以我在教育网内没法测试更多功能了,只能写这么多,发出去的包wireshark显示为STUN,但由于测试用Server是国外的服务器,不加代理没法访问,所以就没办法了…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
 
#define STUN_SERVER_IP "132.177.123.13"
#define STUN_SERVER_PORT 3478
 
int socketfd;
 
struct stun_header{
	unsigned short type;
	unsigned short length;
	unsigned int transId1;
	unsigned int transId2;
	unsigned int transId3;
	unsigned int transId4;
};
 
struct mapped_address{
	unsigned padding : 8;
	unsigned family : 8;
	unsigned short port;
	unsigned int address;
};
 
int 
init_socket(){
	socketfd = socket(AF_INET , SOCK_DGRAM , 0);
}
 
int 
stun_rand(){
	srand(time(NULL));
	return rand();
}
 
struct stun_header* 
build_stun_header(unsigned short type){
 
	struct stun_header *header ;
 
	header = (struct stun_header*)malloc(sizeof(struct stun_header));
	header->type = htons(type);
	header->length = 0;
	header->transId1 = stun_rand();
	header->transId2 = stun_rand();
	header->transId3 = stun_rand();
	header->transId4 = stun_rand();
 
	return header;
}
 
int 
udp_send(void *data , int len
		, const char *ip , int port){
 
	struct sockaddr_in addr;
	addr.sin_family = AF_INET;
	addr.sin_addr.s_addr = inet_addr(ip);
	addr.sin_port = htons(port);
 
	return sendto(socketfd , data , len , 0
			, (struct sockaddr*)&addr , sizeof(struct sockaddr));
 
}
 
int 
main(int argc , char *argv[]){
 
	init_socket();
	struct stun_header recv_header;
	struct sockaddr_in addr;
	size_t length;
 
	struct stun_header* header
		= build_stun_header(0x0001);
 
	udp_send(header , sizeof(struct stun_header)
			, STUN_SERVER_IP , STUN_SERVER_PORT);
 
	length = sizeof(struct sockaddr);
	recvfrom(socketfd , (void*)&recv_header , sizeof(recv_header) , 0
			, (struct sockaddr*)&addr , &length);
 
	return 0;
}

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

HTTP代理原理及Base64编码

四月 14th, {2010 2 条评论 13,889 人阅读过  

有网友留言希望我在飞信的新版本中加上代理功能,这两天折腾了一下,把HTTP代理给加上了,SOCKS代理太麻烦了,暂时应该不会考虑加它了,而且用的人也不多,加不加意义不大了。

写一下HTTP代理的原理,其实原理很简单,就是通过HTTP协议与代理服务器建立连接,协议信令中包含要连接到的远程主机的IP和端口号,如果有需要身份验证的话还需要加上授权信息,服务器收到信令后首先进行身份验证,通过后便与远程主机建立连接,连接成功之后会返回给客户端200,表示验证通过,就这么简单,下面是具体的信令格式:

CONNECT 59.64.128.198:21 HTTP/1.1
Host: 59.64.128.198:21
Proxy-Authorization: Basic bGV2I1TU5OTIz
User-Agent: OpenFetion

其中Proxy-Authorization是身份验证信息,Basic后面的字符串是用户名和密码组合后进行base64编码的结果,也就是对username:password进行base64编码。

其实编码对安全性没什么意义,base64严格意义上都已经不能算是加密了,现在信息安全这么受重视的年代,不需要密钥的加密算法还是叫编码更贴切一些,抓到这种包之后瞬间就可以得到用户名和密码。

HTTP/1.0 200 Connection established

OK,客户端收到收面的信令后表示成功建立连接,接下来要发送给远程主机的数据就可以发送给代理服务器了,代理服务器建立连接后会在根据IP地址和端口号对应的连接放入缓存,收到信令后再根据IP地址和端口号从缓存中找到对应的连接,将数据通过该连接转发出去。

下面说一下base64这种编码方式,它还是很常用的,优点是使用起来简单,缺点是编码效率低劲(每3个字节会编码成4个字节),安全性差(这个刚才已经说过了)。

以前用.NET和java的时候形形色色的编码加密散列都见过也差不多都用过,不过高级语言会提供相应的类,一两句话就可以实现加密解密,当然在C里面也可以用OpenSSL来实现,同样简单,飞信里面也用到了base64编码,就是在2010版本里面要求输入图片验证码的时候用到的,服务器端会将生成的图片验证码图片的二进制数据流进行base64编码,以明文的方式放到xml中发送给客户端,客户端对其进行解码,然后就可以得到验证码图片(还是觉得这个过程有点多此一举)。

今天看了一下base64的编码规则,把3个8位字节(3*8=24)转化为4个6位的字节(4*6=24),之后在6位的前面补两个0,形成8位一个字节的形式,这样就形成了4个8位的字节,然后再将每个8位字节转换成10进制,再从编码表中找到对应的字符就可以了,编码的时候3个字节一组进行编码,当不足3个字节的时候,进行完上面的操作后,将剩余的位用0补齐(注意:并不是补到32位),于是2个字节编成3个字节,不足四个字节的用’='补齐,哈,感觉自己写得乱七八糟的,不过这种东西在网上一搜一大片,也没有必要细说了,另外附上刚才写的一个编码的小程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
 #include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
char* base64_encode(const char *src);
 
int main(int argc , char *argv[])
{
	const char in[] = "levin";
	char *res = base64_encode(in);
	printf("%s\n" , res);
	free(res);
	return 0;
}
char* base64_encode(const char *src){
 
	char* dest;
	char in[4];
	long buf = 0 , tmp = 0;
	int i = 0 , j = 0 , count = 0;
	char table[] = {
	    'A' , 'B' , 'C' , 'D' , 'E' , 'F' , 'G'
	  , 'H' , 'I' , 'J' , 'K' , 'L' , 'M' , 'N'
	  , 'O' , 'P' , 'Q' , 'R' , 'S' , 'T' , 'U'
	  , 'V' , 'W' , 'X' , 'Y' , 'Z' , 'a' , 'b'
	  , 'c' , 'd' , 'e' , 'f' , 'g' , 'h' , 'i'
	  , 'j' , 'k' , 'l' , 'm' , 'n' , 'o' , 'p'
	  , 'q' , 'r' , 's' , 't' , 'u' , 'v' , 'w'
	  , 'x' , 'y' , 'z' , '0' , '1' , '2' , '3'
	  , '4' , '5' , '6' , '7' , '8' , '9' , '+'
	  , '/' , '='
	};
 
	count = strlen(src) / 3 + (strlen(src) % 3 == 0 ? 0 : 1);
	dest = (char*)malloc(count * 4 + 1);
 
	for( ; j < count ; j++){
 
		bzero(in , sizeof(in));
		strncpy(in , src + j * 3 , 3 );
 
		buf = 0 , buf = 0 , i = 0; 
 
		for(; i < strlen(in) ; i++){
			tmp = (long)in[i];
			tmp <<= (16 - i * 8);
			buf |= tmp;
		}
 
		for(i = 0; i < 4 ; i ++){
			if(strlen(in) + 1 > i){
				tmp = buf >> (18 - i * 6);
				tmp &= 0x3F;
				dest[j * 4 + i] = table[tmp];
			}else{
				dest[j * 4 + i] = '=';
			}
		}
	}
	return dest;
}

上面的程序会输出bGV2aW4=,因为最后一组不足3字节,用=补了一个字节。

程序很简单,写它纯粹是为了强化一下对base64编码规则的记忆,解码的就不写了。

我程序里面是直接用OpenSSL来干的,少量数据进行编码的时候一个函数就够了,很方便:

1
EVP_EncodeBlock(out , (unsigned char*)in , strlen(in));

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

飞信2010身份验证过程及算法详述

三月 14th, {2010 23 条评论 65,710 人阅读过  

本人还是在校学生,由于滤涉世未深,对互联网还心存敬畏,没能像当初分析2006飞信协议的大哥那样洒脱,出于各种顾虑,当初在写飞信登录协议相关的文章的时候,都没详细地阐述登录过程,那篇文章当初也只是做为自己业余爱好的一个总结,却没想会受到大家如此的关注,很多人留言或发邮件问我具体的身份验证过程及算法,我也无暇一一详细解答,后来想了想,互联网应该是一个开放的环境,大家对飞信的关注其实也是为了给更多人提供方便或者学习交流,我想我也无须吝啬,就在这里把详细地身份验证过程阐述一下,仅用于学习交流,希望大家把它用在正途。

另外,我本人也在用业余时间写了一个Linux环境下的飞信,期待大家的加入,有兴趣的可以在这里留言。

具体身份验证流程我在下面这篇文章里面说得比较详细了,有需要的话可以先阅读一下这篇文章:

http://basiccoder.com/fetion2010-auth-algorithm.html

在里面有两个重要的过程:

1.密码的处理方式。

密码并不是以明文的方式进行加密的,而是用SHA1做了散列之后才进行加密的,散列的过程与2008略有不同,文字表达能力有限,就贴几个C函数吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
char* hash_password_v1(const unsigned char* b0 , int b0len
                                   , const unsigned char* password , int psdlen) 
{
	unsigned char* dst = (unsigned char*)malloc(b0len + psdlen + 1);
	unsigned char tmp[20];
	char* res;
	memset(tmp , 0 , sizeof(tmp));
	memset(dst , 0 , b0len + psdlen + 1);
	memcpy(dst , b0 , b0len);
	memcpy(dst + b0len , password , psdlen);
	SHA_CTX ctx;
	SHA1_Init(&ctx);
	SHA1_Update(&ctx , dst , b0len + psdlen );
	SHA1_Final(tmp , &ctx);
	free(dst);
	res = hextostr(tmp , 20);
	return res;
}
char* hash_password_v2(const char* userid , const char* passwordhex) 
{
	int id = atoi(userid);
	char* res;
	unsigned char* bid = (unsigned char*)(&id);
	unsigned char ubid[4];
	int bpsd_len;
	unsigned char* bpsd = strtohex(passwordhex , &bpsd_len);
	memcpy(ubid , bid , 4);
	res = hash_password_v1(ubid , sizeof(id) , bpsd , bpsd_len);
	free(bpsd);
	return res;
}
char* hash_password_v4(const char* userid , const char* password)
{
	const char* domain = "fetion.com.cn:";
	char *res , *dst;
	unsigned char* udomain = (unsigned char*)malloc(strlen(domain));
	unsigned char* upassword = (unsigned char*)malloc(strlen(password));
	memset(udomain , 0 , strlen(domain));
	memcpy(udomain , (unsigned char*)domain , strlen(domain));
	memset(upassword , 0 , strlen(password));
	memcpy(upassword , (unsigned char*)password , strlen(password));
	res = hash_password_v1(udomain , strlen(domain) , upassword , strlen(password));
	free(udomain);
	free(upassword);
	if(userid == NULL || strlen(userid) == 0)
	{
		return res;
	}
	dst = hash_password_v2(userid , res);
	free(res);
	return dst;
}

第三个函数是用来对密码进行散列处理的函数,输入是userid和密码,这个函数返回一个40字节的16进制字符串。

另外还有两个函数是在unsigned char串和char串之间相互转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
unsigned char* strtohex(const char* in , int* len) 
{
	unsigned char* out = (unsigned char*)malloc(strlen(in)/2 );
	int i = 0 , j = 0 , k = 0 ,length = 0;
	char tmp[3] = { 0 };
	memset(out , 0 , strlen(in) / 2);
	while(i < (int)strlen(in))
	{
		tmp[k++] = in[i++];
		tmp[k] = '\0';
		if(k == 2)
		{
			out[j++] = (unsigned char)strtol(tmp , (char**)NULL , 16);
			k = 0;
			length ++;
		}
	}
	if(len != NULL )
		*len = length;
	return out;
}
char* hextostr(const unsigned char* in , int len) 
{
	char* res = (char*)malloc(len * 2 + 1);
	int i = 0;
	memset(res , 0 , len * 2 + 1);
	while(i < len)
	{
		sprintf(res + i * 2 , "%02x" , in[i]);
		i ++;
	};
	i = 0;
	while(i < (int)strlen(res))
	{
		res[i] = toupper(res[i]);
		i ++;
	};
	return res;
}

然后是计算response过程:

1
2
3
/*这当然是不符合语法的,这样写大家应该就能明白*/
unsigned char* response_tmp = (unsigned char*)nonce
          + strtohex(hashed_password) + strtohex(aedkey);

最后把response_tmp通过服务器发过来的公钥进行RSA加密后得到response,传回服务器就可以通过验证了。

具体的信令格式都是明文,大家自己抓包看就好了,我在这里也就不多说了。

分类: Protocol 标签: , ,