我们都知道,同一台计算机上的进程可以通过IPC(进程间通信)机制进行通信;而不同机算计上运行的进程则通过网络IPC,即套接字(socket)进行通信。Linux下的socket API是基于BSD套接口而是实现的,通过这些统一的API就可以轻松实现进程间的网络通信。此外,socket API即可用于面向连接(TCP)的数据传输,又可用于无连接(UDP)的数据传输。一般使用Client/Server交互模型进行通信。
本文以及下文将实现一个面向连接的C/S通信模型。本文首先介绍服务器端的实现。
1.创建套接字
#include < sys/socket.h > int socket(int domain, int type, int protocol);
通过socket函数可以创建一个套接字描述符,这个描述符类似文件描述符。通过这个套接字描述符就可以对服务器进行各种相关操作。
该函数包含三个参数,domain参数用于指定所创建套接字的协议类型。通常选用AF_INET,表示使用IPv4的TCP/IP协议;如果只在本机内进行进程间通信,则可以使用AF_UNIX。参数type用来指定套接字的类型,SOCK_STREAM用于创建一个TCP流的套接字,SOCK_DGRAM用于创建UDP数据报套接字。参数protocol通常取0。对于本文所描述的服务器,创建套接字的示例代码如下:
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { printf("socket error!\n"); exit(1); }
2.绑定套接字
对于服务器而言,它的IP地址和端口号一般是固定的。服务器的IP即为本地IP,而服务器的端口号则需要显示的指定。通过bind函数可将服务器套接字和一个指定的端口号进行绑定。
在具体介绍绑定函数之前,先说明一下socket中的套接字地址结构。由于套接字是通过IP地址和端口号来唯一确定的,因此socket提供了一种通用的套接字地址结构:
struct sockaddr { sa_family_t sa_family; char sa_data[14]; }
sa_family指定了套接字对应的协议类型,如果使用TCP/IP协议则改制为AF_INET;sa_data则用来存储具体的套接字地址。不过在实际应用中,每个具体的协议族都有自己的协议地址格式。比如TCP/IP协议组对应的套接字地址结构体为:
struct sockaddr_in { short int sin_family; /* Address family */ unsigned short int sin_port; /* Port number */ struct in_addr sin_addr; /* Internet address */ unsigned char sin_zero[8]; /* Same size as struct sockaddr */ }; struct in_addr { unsigned long s_addr; };
该地址结构和sockaddr结构均为16字节,因此通常在编写基于TCP/IP协议的网络程序时,使用sockaddr_in来设置具体地址,然后再通过强制类型转换为sockaddr类型。
绑定函数的函数原型如下:
#include < sys/socket.h > int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数sockfd即服务器的套接字描述符;addr参数指定了将socket绑定到的本地地址;addrlen则为所使用的地址结构的长度。示例代码如下:
memset(&my_addr, 0, sizeof(struct sockaddr_in)); my_addr.sin_family = AF_INET; my_addr.sin_port = htons(SERV_PORT); my_addr.sin_addr.s_addr = htonl(INADDR_ANY); if(bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr_in)) == -1) { printf("bind error!\n"); exit(1); }
注意在上述代码中,将IP地址设置为INADDR_ANY,这样就既适合单网卡的计算机又适合多网卡的计算机。
3.在套接字上监听
对于C/S模型来说,通常是客户端主动的对服务器端发送连接请求,服务器接收到请求后再具体进行处理。服务器只有调用了listen函数才能宣告自己可以接受客户端的连接请求,也就是说,服务器此时处于被动监听状态。listen函数的原型如下:
#include < sys/socket.h > int listen(int sockfd, int backlog);
sockfd为服务器端的套接字描述符,backlog指定了该服务器所能连接客户端的最大数目。超过这个连接书目后,服务器将拒绝接受客户端的连接请求。示例代码如下:
#define BACKLOG 10 if(listen(sockfd, BACKLOG) == -1) { printf("listen error!\n"); exit(1); }
4.接受连接
listen函数只是将服务器套接字设置为等待客户端连接请求的状态,真正接受客户端连接请求的是accept函数:
#include < sys/socket.h > int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept函数中所使用的参数都在上面的API中有所描述。accept函数执行成功后将返回一个代表客户端的套接字描述符,服务器进程通过该套接字描述符就可以与客户端进行数据交换。
5.数据传输
由于socket适用多个数据传输协议,则不同的协议就对应不同的数据传输函数。与TCP协议对应的发送数据和接受数据的函数如下:
#include < sys/socket.h > #include < sys/types.h > ssize_t send(int sockfd, const void *buf, size_t len, int flags); ssize_t recv(int sockfd, void *buf, size_t len, int flags);
从这两个函数的原型可以看书,socket中的数据传输函数与普通文件的读写函数类似,只不过第一个参数需要传递套接字描述符;buf指定数据缓冲区;len为所传输数据的长度;flag一般取0。示例代码如下:
while (1) { sin_size = sizeof(struct sockaddr_in); if ((client_fd = accept(sockfd, (struct sockaddr *)&remote_addr, &sin_size)) == -1) { printf("accept error!\n"); continue; } /* *进行相应的数据处理; */ }
如示例代码所示,通过while循环使得服务器对客户端进行持续监听。如果客户端有连接请求则新建一个代表客户端的套接字描述符,进而进行对客户端数据的接受和发送。
上述的几个函数属于网络编程中最基本的也是最关键的几个API,依次通过上述的方法就可以完成服务器端的程序的编写,具体的过程还可以参考下图:
正如上图所示,在处理完客户端的数据传输请求后,必须通过close函数关闭客户端的连接。
参考:
1.Linux C编程实战;童永清 著;人民邮电出版社;
是原创么?你有教育天赋
[回复一下]
edsionte 回复:
8月 6th, 2012 at 15:32
@sinoon, yep,根据自己的实战写的。
[回复一下]
sinoon 回复:
8月 8th, 2012 at 16:05
@edsionte, 大哥,求一下服务端的源代码,做了好几天都没有做好,第一次学QT,还是在LINUX下。。。好人一生平哈~
[回复一下]
edsionte 回复:
8月 8th, 2012 at 16:34
@sinoon, 服务器端源码直接是用LinuxC编写的。跟QT无关。
[回复一下]
sinoon 回复:
8月 8th, 2012 at 16:36
@edsionte, 正合我意啊。最近要做个socket,必须在linux下,必须用Qt写,但不能用Qt的函数,要用c/c++的。。。
so。。。博主,什么叫知音?
拜托~~~~
[回复一下]
edsionte 回复:
8月 8th, 2012 at 16:46
@sinoon, 源码因为涉及项目所以不能给你。但是基本框架我在其他文章有写,你先看看,具体问题你可以在这里提出来。
好吧,还望赐教~~
accept()函数运行时,会卡住界面,请问怎么办?用子进程还是多线程?希望详细一些。
发送文字在Qt下是怎么处理的?我send里 如果写,ui->textBowser的话,送过去就一定是乱码,而且还都一样长(汗),请问怎么处理?估计是我基础太差。
IP地址如果输入,怎么处理成能被套接字使用的格式?我一直弄不明白,Qt说那个是QSstring。。。
这几个困扰我了2天,还没有找到答案。。。baidu,googl,都没成功。。。好吧,我承认是我学习能力有问题。。。
希望能详解,谢谢!
[回复一下]
edsionte 回复:
8月 9th, 2012 at 09:23
@sinoon, 1.我用的是多进程,服务器每处理一个请求时候fork一个子进程去处理。
2.客户端用QT+原生的Socket编写,用Qt做界面,通信部分都用的是原生Socket接口。因此C和S端都是用的原生socket接口发送接手。Qt的通信接口我感觉用起来不方便。
上面是我个人意见,请参考。
[回复一下]
sinoon 回复:
8月 9th, 2012 at 09:29
@edsionte, 好,请问那个fork怎么写?我整了半天都没有弄好,关于fork的这段能讲讲么?
[回复一下]
sinoon 回复:
8月 9th, 2012 at 09:35
@edsionte, 另外,我不知道是不是这样,当accept()时,服务端处于等待连接,此时程序停在这里,当有一个连接出现,程序才会继续执行?
是这样么?
[回复一下]
edsionte 回复:
8月 9th, 2012 at 09:39
@sinoon, 你还是先看看网络编程的基本框架吧。
[回复一下]
@sinoon, fork如何写这些都是网络编程最基本的问题,你可以先试试最简单的小程序,再逐渐丰富你的功能。如何写?你看看任何一本讲网路编程的书都会有。
[回复一下]
有什么这方面的书推荐么?
[回复一下]
edsionte 回复:
8月 9th, 2012 at 10:57
@sinoon, 很多啊。只要是网络编程的书都会有这个。比如Unix网络编程 卷1或者Linux网络编程。后者比较容易入门。
[回复一下]
sinoon 回复:
8月 9th, 2012 at 11:40
@edsionte, 请教一下,在fork里,为什么不能对textBowser进行操作?
textEdit里的文字怎么处理才能正常发送给服务器?
[回复一下]
edsionte 回复:
8月 9th, 2012 at 11:44
@sinoon, 服务器:提取字符流,send;客户端:recv;
[回复一下]
recv(isockclint,buf,maxsize,0);
不是很明白,客户端:
send(iclintsock,ui->textBowser,100,0);
就是把textBowser里的内容发过去吧?如果这么写就全是乱码,好像要处理一下,可是我不知道怎么处理?
send(iclintsock,“要发送的内容”,100,0);
如果这样写倒是没有问题,但是不能控制说什么。。。
[回复一下]
sinoon 回复:
8月 9th, 2012 at 15:02
@edsionte, 在fork里不能对ui里控件作影响,你是怎么办到的?
[回复一下]