Archive for the ‘Linux下C编程’ category

存储映射I/O

23 10 月, 2011

一个进程拥有独立并且连续虚拟地址空间,在32位体系结构中进程的地址空间是4G。不过,内核在管理进程的地址空间时是以内存区域为单位。内存区域是进程整个地址空间中一个独立的内存范围,它在内核中使用vm_area_struct数据结构来描述。每个内存区域都有自己访问权限以及操作函数,因此进程只能对有效范围的内存地址进行访问。

存储映射I/O是一种基于内存区域的高级I/O操作,它将磁盘文件与进程地址空间中的一个内存区域相映射。当从这段内存中读数据时,就相当于读磁盘文件中的数据,将数据写入这段内存时,则相当于将数据直接写入磁盘文件。这样就可以在不使用基本I/O操作函数read和write的情况下执行I/O操作。

1.基本实现方法

实现存储映射I/O的核心操作是通过mmap系统调用将一个给定的磁盘文件映射到一个存储区域中。

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

关于该函数定义中各个参数说明Linux上的man手册已经解释的很清楚,在此不再赘述。这里需要特别说明的是prot和flags参数。prot用来指定对映射区域的保护要求,但是它的保护范围不能超过文件open时指定的打开权限。比如以只读(PROT_READ)方式打开一个文件,那么以读写(PROT_READ|PROT_WRITE)方式保护内存区域是不合法的。flags用来指定内存区域的多种属性,两个典型的取值是MAP_SHARED和MAP_PRIVATE。MAP_SHARED标志指定了进程对内存区域的修改会影响到映射文件。而当对flags指定MAP_PRIVATE时,进程会为该映射内存区域创建一个私有副本,对该内存区的所有操作都是在这个副本上进行的,此时对内存区域的修改并不会影响到映射文件。

下面列出一个简单的示例程序,它将磁盘文件映射到一个内存区域中,通过mmap返回的指针先读文件,再写文件。可以看到对文件的读和写操作都是通过内存映射I/O的方式完成的。

int main()
{
	int fd;
	char *buf = NULL;
	int i;

	//打开一个文件
	if (-1 == (fd = open("./mapping_file.txt", O_RDWR))) {
		printf("open file error!\n");
		exit(1);
	}

	//将文件映射到进程的一个内存区域
	buf = mmap(NULL, 100, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
	if (!buf) {
		printf("mmap error!\n");
		exit(1);
	}

	//对映射内存读数据
	for (i = 0; i < 100; i++)
	printf("%c", buf[i]);

	//对映射内存写数据
	if (buf[0] == 'H')
		buf[0] = 'h';
	else
		buf[0] = 'H';

	system("cat ./mapping_file.txt");
	return 0;
}

2.使用内存映射I/O进行文件拷贝

使用基本I/O操作函数如何实现一个类似cp命令的程序?比如我们要将A文件复制到B文件,那么程序的基本框架是这样的:

1.open()文件A和文件B

2.将A文件的内容read()到buffer

3.将buffer中的数据write()到文件B

4.close()文件A和文件B

如果使用内存映射I/O来实现cp命令,那么它的基本框架是这样的:

1.open()文件A和文件B

2.mmap()文件A和文件B,其中src和dest分别为两个文件映射到内存的地址

3.将以src为起始的len长字节数据memcpy()到dest

4.close()文件A和文件B

示例程序如下:

int main()
{
	int srcfd, destfd;
	struct stat statbuf;
	char *src = NULL, *dest = NULL;

	//打开两个文件
	if (-1 == (srcfd = open("./src.txt", O_RDONLY))) {
		printf("open src file error!\n");
		exit(1);
	}

	if (-1 == (destfd = open("./dest.txt", O_RDWR | O_CREAT | O_TRUNC))) {
		printf("open dest file error!\n");
		exit(1);
	}

	//获取原始文件的长度
	if (-1 == fstat(srcfd, &statbuf)) {
		printf("fstat src file error!\n");
		exit(1);
	}

	//设置输出文件的大小
	if (-1 == lseek(destfd, statbuf.st_size - 1, SEEK_SET)) {
		printf("lseek error!\n");
		exit(1);
	}
	if (-1 == write(destfd, "", 1)) {
		printf("write error!\n");
		exit(1);
	}

	if ((src = mmap(0, statbuf.st_size, PROT_READ, MAP_SHARED, srcfd, 0)) == MAP_FAILED) {
		printf("mmaping src file error!\n");
		exit(1);
	}

	if ((dest = mmap(0, statbuf.st_size + 2, PROT_READ | PROT_WRITE, MAP_SHARED, destfd, 0)) == MAP_FAILED) {
		printf("mmaping dest file error!\n");
		exit(1);
	}

	memcpy(dest, src, statbuf.st_size);

	printf("src file:\n");
	system("cat ./src.txt");
	printf("dest file:\n");
	system("cat ./dest.txt");

	close(srcfd);
	close(destfd);

	return 0;
}

按照上述列出的基本框架,该程序首先打开两个文件,通过fstat()获得源文件的长度。因为在mmap两个文件以及设置目的文件长度时都需要源文件的长度。设置目的文件通过lseek()即可完成,如果没有设置目的文件的长度,那么将会产生总线错误(引发信号SIGBUS)。然后分别mmap()两个文件到进程的地址空间,最后调用memcpy()将源文件内存区的数据拷贝到目的文件内存区。

通过基本I/O和内存映射I/O均可以进行文件拷贝,那么两者的效率谁更高一些?这其实是个很难回答的问题。不管是使用基本I/O操作函数还是mmap方式,操作系统都会在内存中进行缓存(cache),而且在不同的应用场景、不同的平台下结果都会收到影响。但是抛开这些因素单从对文件操作这个方面来说,内存映射方式比read和write方式要快。

如果使用read/write方式进行文件拷贝,首先将数据从用内核缓冲区复制到用户空间缓冲区,这是read的过程;再将数据从用户空间缓冲区复制到内核缓冲区,这是write过程。如果是内存映射方式,则直接是用户空间中数据的拷贝,也就是将源文件所映射内存中的数据拷贝到目的文件所映射的内存中。这样就避免了用户空间和内核空间之间数据的来回拷贝。

但是内存映射方式并不是完美的,它所映射的文件只能是固定大小,因为文件所映射的内存区域大小在mmap时通过len已经指定。另外,文件映射的内存区域的大小必须以页大小为单位。比如系统页大小为4096字节,假定映射文件的大小为20字节,那么该页剩余的4076字节全部被填充为0。虽然通过映射地址可以访问并修改剩余字节,但是任何变动都不会在映射文件中反应出来。由此可见,使用内存映射进行大数据量的拷贝比较有效。

进程在Linux内核中的角色扮演

2 9 月, 2011

在Linux内核中,内核将进程、线程和内核线程一视同仁,即内核使用唯一的数据结构task_struct来分别表示他们;内核使用相同的调度算法对这三者进行调度;并且内核也使用同一个函数do_fork()来分别创建这三种执行线程(thread of execution)。执行线程通常是指任何正在执行的代码实例,比如一个内核线程,一个中断处理程序或一个进入内核的进程。

这样处理无疑是简洁方便的,并且内核在统一处理这三者之余并没有失去他们本身所具有的特性。本文将结合进程、线程和内核线程的特性浅谈进程在内核中的角色扮演问题。

1.进程描述符task_struct的多角色扮演

上述三种执行线程在内核中都使用统一的数据结构task_struct来表示。task_struct结构即所谓的进程描述符,它包含了与一个进程相关的所有信息。进程描述符中不仅包含了许多描述进程属性的字段,而且还有一系列指向其他数据结构的指针。下面将简单介绍进程描述符中几个比较特殊的字段,它们分别指向代表进程所拥有的资源的数据结构。

mm字段:指向mm_struct结构的指针,该类型用来描述进程整个的虚拟地址空间。

fs字段:指向fs_struct结构的指针,该类型用来描述进程所在文件系统的根目录和当前进程所在的目录信息。

files字段:指向files_struct结构的指针,该类型用来描述当前进程所打开文件的信息。

signal字段:指向signal_struct结构(信号描述符)的指针,该类型用来描述进程所能处理的信号。

对于普通进程来说,上述字段分别指向具体的数据结构以表示该进程所拥有的资源。

对应每个线程而言,内核通过轻量级进程与其进行关联。轻量级进程之所轻量,是因为它与其他进程共享上述所提及的进程资源。比如进程A创建了线程B,则B线程会在内核中对应一个轻量级进程。这个轻量级进程很自然的对应一个进程描述符,只不过B线程的进程描述符中的某些代表资源指针会和A进程中对应的字段指向同一个数据结构,这样就实现了多线程之间的资源共享。

由于内核线程只运行在内核态,并且只能由其他内核线程创建,所以内核线程并不需要和普通进程那样的独立地址空间。因此内核线程的进程描述符中的mm指针即为NULL。内核线程是否共享父内核线程的某些资源,则通过向内核线程创建函数kernel_thread()传递参数来决定。

通过上面的分析可以发现,内核中使用统一的进程描述符来表示进程、线程和内核线程,根据他们不同的特性,其进程描述符中某些代表资源的字段的指向会有所不同,以实现扮演不同角色。

2. do_fork()的多角色扮演

进程、线程以及内核线程都有对应的创建函数,不过这三者所对应的创建函数最终在内核都是由do_fork()进行创建的,具体的调用关系图如下:

从图中可以看出,内核中创建进程的核心函数即为看do_fork(),该函数的原型如下:

long do_fork(unsigned long clone_flags,
               unsigned long stack_start,
               struct pt_regs *regs,
               unsigned long stack_size,
               int __user *parent_tidptr,
               int __user *child_tidptr)

该函数的参数个数是固定的,每个参数的功能如下:

clone_flags:代表进程各种特性的标志。低字节指定子进程结束时发送给父进程的信号代码,一般为SIGCHLD信号,剩余三个字节是若干个标志或运算的结果。

stack_start:子进程用户态堆栈的指针,该参数会被赋值给子进程的esp寄存器。

regs:指向通用寄存器值的指针,当进程从用户态切换到内核态时通用寄存器中的值会被保存到内核态堆栈中。

stack_size:未被使用,默认值为0。

parent_tidptr:该子进程的父进程用户态变量的地址,仅当CLONE_PARENT_SETTID被设置时有效。

child_tidptr:该子进程用户态变量的地址,仅当CLONE_CHILD_SETTID被设置时有效。

既然进程、线程和内核线程在内核中都是通过do_fork()完成创建的,那么do_fork()是如何体现其功能的多样性?其实,clone_flags参数在这里起到了关键作用,通过选取不同的标志,从而保证了do_fork()函数实现多角色——创建进程、线程和内核线程——功能的实现。clone_flags参数可取的标志很多,下面只介绍几个与本文相关的标志。

CLONE_VIM:子进程共享父进程内存描述符和所有的页表。

CLONE_FS:子进程共享父进程所在文件系统的根目录和当前工作目录。

CLONE_FILES:子进程共享父进程打开的文件。

CLONE_SIGHAND:子进程共享父进程的信号处理程序、阻塞信号和挂起的信号。使用该标志必须同时设置CLONE_VM标志。

如果创建子进程时设置了上述标志,那么子进程会共享这些标志所代表的父进程资源。

2.1 进程的创建

在用户态程序中,可以通过fork()、vfork()和clone()三个接口函数创建进程,这三个函数在库中分别对应同名的系统调用。系统调用函数通过128号软中断进入内核后,会调用相应的系统调用服务例程。这三个函数对应的服务历程分别是sys_fork()、sys_vfork()和sys_clone()。

 int sys_fork(struct pt_regs *regs)
 {
         return do_fork(SIGCHLD, regs->sp, regs, 0, NULL, NULL);
 }

 int sys_vfork(struct pt_regs *regs)
 {
         return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs->sp, regs, 0,
                        NULL, NULL);
 }

 long
 sys_clone(unsigned long clone_flags, unsigned long newsp,
           void __user *parent_tid, void __user *child_tid, struct pt_regs *regs)
 {
         if (!newsp)
                 newsp = regs->sp;
         return do_fork(clone_flags, newsp, regs, 0, parent_tid, child_tid);
 }

通过上述系统调用服务例程的源码可以发现,三个服务历程内部都调用了do_fork(),只不过差别在于第一个参数所传的值不同。这也正好导致由这三个进程创建函数所创建的进程有不同的特性。下面对每种进程作以简单说明。

fork():由于do_fork()中clone_flags参数除了子进程结束时返回给父进程的SIGCHLD信号外并无其他特性标志,因此由fork()创建的进程不会共享父进程的任何资源。子进程会完全复制父进程的资源,也就是说父子进程相对独立。不过由于写时复制技术(Copy On Write,COW)的引入,子进程可以只读父进程的物理页,只有当两者之一去写某个物理页时,内核此时才会将这个页的内容拷贝到一个新的物理页,并把这个新的物理页分配给正在写的进程。

vfork():do_fork()中的clone_flags使用了CLONE_VFORK和CLONE_VM两个标志。CLONE_VFORK标志使得子进程先于父进程执行,父进程会阻塞到子进程结束或执行新的程序。CLONE_VM标志使得子进程共享父进程的内存地址空间(父进程的页表项除外)。在COW技术引入之前,vfork()适用子进程形成后立马执行execv()的情形。因此,vfork()现如今已经没有特别的使用之处,因为写实复制技术完全可以取代它创建进程时所带来的高效性。

clone():clone通常用于创建轻量级进程。通过传递不同的标志可以对父子进程之间数据的共享和复制作精确的控制,一般flags的取值为CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND。由上述标志可以看到,轻量级进程通常共享父进程的内存地址空间、父进程所在文件系统的根目录以及工作目录信息、父进程当前打开的文件以及父进程所拥有的信号处理函数。

2.2 线程的创建

每个线程在内核中对应一个轻量级进程,两者的关联是通过线程库完成的。因此通过pthread_create()创建的线程最终在内核中是通过clone()完成创建的,而clone()最终调用do_fork()。

2.3 内核线程的创建

一个新内核线程的创建是通过在现有的内核线程中使用kernel_thread()而创建的,其本质也是向do_fork()提供特定的flags标志而创建的。

 int kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
 {
        /*some register operations*/
         return do_fork(flags | CLONE_VM | CLONE_UNTRACED, 0, &regs, 0, NULL, NULL);
 }

从上面的组合的flag可以看出,新的内核线程至少会共享父内核线程的内存地址空间。这样做其实是为了避免赋值调用线程的页表,因为内核线程无论如何都不会访问用户地址空间。CLONE_UNTRACED标志保证内核线程不会被任何进程所跟踪,

3. 进程的调度

由于进程、线程和内核线程使用统一数据结构来表示,因此内核对这三者并不作区分,也不会为其中某一个设立单独的调度算法。内核将这三者一视同仁,进行统一的调度。

参考资料:

1. 深入理解Linux内核

2. Linux内核设计与实现

使用gcc同时编译多个文件

28 7 月, 2011

平时在写C程序的时候都是将所有代码全部写入一个.c文件中,这样写对小程序来说是适合的。但是对于比较大的项目,将所有代码写在一个文件不利于代码的维护。

最近由于项目需求,将一个服务器程序按照功能整理成了几个.c文件和.h文件。该服务器程序包含以下文件:

.
|-- config_system.c
|-- generic_function.c
|-- log_system.c
|-- server.c
`-- server.h

代码整理完毕之后,开始对源文件进行重新编译。在编译的过程中遇到了一些问题,现在总结如下。

1. 自定义头文件的使用

server.h文件包含服务器程序中所有自定义函数原型的声明和一些常量定义,将该自定义函数放在当前程序的目录下即可。自定义头文件的使用方法就和普通头文件的使用方法一样,加在程序最开始的位置即可,不过要将包含头文件的尖括号<>改成双引号“ ”。

通过这个实践,也可以更好的理解包含头文件的两种方法。对于尖括号包含的头文件,gcc在系统默认的目录中(/usr/include)查找相应的头文件;对于双引号包含的头文件,编译器首先会在当前目录下或指定的目录下(如果有指定)去查找头文件,如果当前目录下没有该头文件则去默认的头文件目录下查找。

刚才我们提到了头文件的指定目录,在大型项目中,头文件可能会更多,往往将头文件单独放在一个目录当中,此时应该使用-Idirname选项来告诉编译器到指定的目录dirname中去查找头文件。比如我们将server.h放在head目录中再进行编译:

gcc -Ihead config_system.c server.c log_system.c generic_function.c -o server

选项I代表include,该选项的作用阶段是预处理阶段。

2. 多个源文件的编译

我们可以先将每个源文件编译成目标文件,最后再生成一个可执行文件。比如:

edsionte@edsionte-desktop:~/server$ gcc -c server.c
edsionte@edsionte-desktop:~/server$ gcc -c config_system.c
edsionte@edsionte-desktop:~/server$ gcc -c log_system.c
edsionte@edsionte-desktop:~/server$ gcc -c generic_function.c
edsionte@edsionte-desktop:~/server$ gcc server.o config_system.o log_system.o generic_function.o -o server

也可以直接使用 一条命令:

edsionte@edsionte-desktop:~/server$ gcc server.c generic_function.c log_system.c config_system.c -o server

不管是那种方法都可以编译源文件,最后连接成可执行文件。在第一种方法中,单独使用-c选项只是编译源文件,并不涉及链接的过程,因此源文件是否包含main函数我们并不关心。第二种方法是将编译链接通过一条命令来完成。

其实在使用gcc编译程序时,整个编译过程分为预编译、编译、汇编和链接四个步骤。上述两种方法都自动完成了前两个步骤。

3. extern的使用

按照上面的命令进行编译程序,出现了下面的错误提示:

config_system.c:80: error: ‘config_filename’ undeclared (first use in this function)

这条错误是在说明config_filename这个变量未声明。config_filename作为一个全局变量在generic_function.c文件中已经声明并定义过,那么如何让编译器知道该变量已经定义过并且可以在config_system.c文件中使用?此时extern关键字就派上了用场。

extern的作用是声明一个变量,它的作用仅仅是声明,而不是像定义该变量时那样为其分配内存空间。一个变量在整个程序中只能定义一次,但是却可以声明多次。

4. 使用脚本编译并运行服务器程序

由于将代码按照功能分离后,每次编译都需要很长一串的命令,因此写个脚本就显得很有必要了。下面的脚本先将原有的可执行文件(如果存在的话)删除,再编译程序,最后运行可执行文件。

#!/bin/bash
test server && rm server
gcc server.c config_system.c log_system.c generic_function.c -o server
echo "compliing success!"
echo "sever is running.."
./server

通过这次“分离”代码的实践,给我最大的感受就是平时看似懂的概念只有到真正使用的时候才真的开始理解。

动手实践字符设备驱动

25 4 月, 2011

今年带软件08级童鞋LinuxOS试验的时候,导师让我写一个最简单的字符设备驱动,以便让从未接触过的初学者快速入门。代码参考如下。

字符设备驱动程序:

#include < linux/init.h >
#include < linux/module.h >
#include < linux/types.h >
#include < linux/fs.h >
#include < asm/uaccess.h >
#include < linux/cdev.h >

MODULE_AUTHOR("Edsionte Wu");
MODULE_LICENSE("GPL");

#define MYCDEV_MAJOR 231 /*the predefined mycdev's major devno*/
#define MYCDEV_SIZE 100

static int mycdev_open(struct inode *inode, struct file *fp)
{
	return 0;
}

static int mycdev_release(struct inode *inode, struct file *fp)
{
	return 0;
}

static ssize_t mycdev_read(struct file *fp, char __user *buf, size_t size, loff_t *pos)
{
	unsigned long p = *pos;
	unsigned int count = size;
	int i;
	char kernel_buf[MYCDEV_SIZE] = "This is mycdev!";

	if(p >= MYCDEV_SIZE)
		return -1;
	if(count > MYCDEV_SIZE)
		count = MYCDEV_SIZE - p;

	if (copy_to_user(buf, kernel_buf, count) != 0) {
		printk("read error!\n");
		return -1;
	}

	/*
	for (i = 0; i < count; i++) {
		__put_user(i, buf);//write 'i' from kernel space to user space's buf;
		buf++;
	}
	*/

	printk("edsionte's reader: %d bytes was read...\n", count);
	return count;

}

static ssize_t mycdev_write(struct file *fp, const char __user *buf, size_t size, loff_t *pos)
{
	return size;
}

/*filling the mycdev's file operation interface in the struct file_operations*/
static const struct file_operations mycdev_fops =
{
	.owner = THIS_MODULE,
	.read = mycdev_read,
	.write = mycdev_write,
	.open = mycdev_open,
	.release = mycdev_release,
};

/*module loading function*/
static int __init mycdev_init(void)
{
	int ret;

	printk("mycdev module is staring..\n");

	ret=register_chrdev(MYCDEV_MAJOR,"edsionte_cdev",&mycdev_fops);
	if(ret<0)
	{
		printk("register failed..\n");
		return 0;
	}
	else
	{
		printk("register success..\n");
	}

	return 0;
}

/*module unloading function*/
static void __exit mycdev_exit(void)
{
	printk("mycdev module is leaving..\n");
	unregister_chrdev(MYCDEV_MAJOR,"edsionte_cdev");
}

module_init(mycdev_init);
module_exit(mycdev_exit);

Makefile文件:

obj-m:=mycdev.o
PWD:=$(shell pwd)
CUR_PATH:=$(shell uname -r)
KERNEL_PATH:=/usr/src/linux-headers-$(CUR_PATH)

all:
	make -C $(KERNEL_PATH) M=$(PWD) modules
clean:
	make -C $(KERNEL_PATH) M=$(PWD) clean

用户态测试程序:

#include < stdio.h >
#include < sys/types.h >
#include < sys/stat.h >
#include < fcntl.h >
#include < stdlib.h >

int main()
{
	int testdev;
	int i, ret;
	char buf[15];

	testdev = open("/dev/mycdev", O_RDWR);

	if (-1 == testdev) {
		printf("cannot open file.\n");
		exit(1);
	}

	if (ret = read(testdev, buf, 15) < 15) {
		printf("read error!\n");
		exit(1);
	}

	printf("%s\n", buf);

	close(testdev);

	return 0;
}

使用方法:

1.make编译mycdev.c文件,并插入到内核;
2.通过cat /proc/devices 查看系统中未使用的字符设备主设备号,比如当前231未使用;
3.创建设备文件结点:sudo mknod /dev/mycdev c 231 0;具体使用方法通过man mknod命令查看;
4.修改设备文件权限:sudo chmod 777 /dev/mycdev;
5.以上成功完成后,编译本用户态测试程序;运行该程序查看结果;
6.通过dmesg查看日志信息;

Linux下的socket编程-基于Qt的客户端

20 3 月, 2011

上文中对面向连接的C/S模型中的服务器进行了简单描述,本文将说明如何编写一个客户端应用程序。与服务器程序不同,客户端程序要求有友好的交互界面,因此本文所示的客户端程序采用Qt和linux C共同完成。客户端的界面部分采用Qt完成,而与服务器间具体的通信部分则通过linux C语言完成。

1.界面设计

通过Qt Designer可快速设计好客户端界面,并对四个按钮所对应的信号和槽函数进行连接。客户端所对应的类为ClientUI。

2.与服务器间通信的编程

由于Qt是对C++的一种扩展,因此我们必须将使用Linux C编写的客户端通信程序封装成类,这里我们将其定义为ClientConnection。

我们已经对客户端的界面和通信部分分别定义了两个类,但是如何将两者结合在一起?方法很简单。将ClientConnection类的对象cli作为ClientUI类的成员,然后在具体的槽函数中对应相应的通信函数。和上文分析服务器编程的方法不同,本文将以分析客户端对应的槽函数的方式来说明如何编写客户端程序。

2.1连接按钮的槽函数

在连接按钮对应的槽函数中,首先创建客户端套接字描述符;然后从输入框中获得服务器的IP地址;再通过connect函数创建一个连接。

connect函数的原型如下:

     #include < sys/socket.h >
     int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

connect函数用于在sockfd套接字上创建一个连接,sockfd即客户端套接字;addr即为服务器端的套接字地址;addrlen为addr的长度。我们将connect函数封装成ClientConnection类中的connectingSocket函数,该成员函数在连接按钮所对应的槽函数中被调用。:

int ClientConnection::connectingSocket()
{
    if (::connect(sockfd, (struct sockaddr *)&serv_addr,
                          sizeof(struct sockaddr)) == -1) {
        return 0;
    }

    return 1;
}

在该槽函数中,可以这样使用连接函数:

    if (cli.connectingSocket() == 1) {
        ui->statusLabel->setText("connecting to server success!");
    } else {
        ui->statusLabel->setText("ERROR:connecting to server fail!");
        //exit(1);
    }

当客户端连接至服务器成功后,客户端就可以跟服务器端进行数据的传送。

2.2 修改按钮的槽函数

该槽函数的工作比较简单,使得文本编辑区可被编辑。

2.3 应用按钮的槽函数

在该槽函数中,将文本编辑区的数据发送至服务器端;或从服务器接受数据显示在这个文本编辑区中。发送和接受数据的API仍然是send和recv函数,不过这里我们将这两个函数封装成ClientConnection类中的sendData成员和recvData成员:

int ClientConnection::recvData(char *buf)
{
    if ((recvbytes = recv(sockfd, buf, MAX_DATA_NUM, 0)) == -1) {
        return 0;
    }

    buf[recvbytes] = '\0';
    return 1;
}

int ClientConnection::sendData(char *msg, int len)
{
    if (send(sockfd, msg, len, 0) == -1) {
        printf("send error!\n");
        return 0;
    }

    return 1;
}

这两个成员函数在该槽函数的适当位置被调用。

2.4退出按钮的槽函数

退出按钮的槽函数中只需断开客户端和服务器之间的连接即可。

参考:

1.Qt Assistant

2.Linux C编程实战;童永清 著;人民邮电出版社;

windows 7 ultimate product key

windows 7 ultimate product key

winrar download free

winrar download free

winzip registration code

winzip registration code

winzip free download

winzip free download

winzip activation code

winzip activation code

windows 7 key generator

windows 7 key generator

winzip freeware

winzip freeware

winzip free download full version

winzip free download full version

free winrar download

free winrar download

free winrar

free winrar

windows 7 crack

windows 7 crack

windows xp product key

windows xp product key

windows 7 activation crack

windows7 activation crack

free winzip

free winzip

winrar free download

winrar free download

winrar free

winrar free

download winrar free

download winrar free

windows 7 product key

windows 7 product key