欢迎转载,请保留作者信息
bill@华中科技大学
http://billstone.cublog.cn
第五篇 网络通信篇
IPC对象只能实现在一台主机中的进程相互通信, 网罗通信对象则打破了这个限制, 它如同电话和邮件, 可以帮助不通屋檐下的人们相互交流.
套接字(Socket)是网络通信的一种机制, 它已经被广泛认可并成为事实上的工业标准.
第十五章 基于TCP的通信程序
TCP是一种面向连接的网络传输控制协议. 它每发送一个数据, 都要对方确认, 如果没有接收到对方的确认, 就将自动重新发送数据, 直到多次重发失败后,才放弃发送.
套接字的完整协议地址信息包括协议, 本地地址, 本地端口, 远程地址和远程端口等内容, 不同的协议采用不同的结构存储套接字的协议地址信息.
Socket是进程间的一个连接, 我们可以采用协议, 地址和端口的形式描述它:
{ 协议, 本地地址, 本地端口, 远程地址, 远程端口 }
当前有三种常见的套接字类型, 分别是流套接字(SOCK_STREAM), 数据报套接字(SOCK_DGRAM)和原始套接字(SOCK_RAW):
1) 流套接字. 提供双向的, 可靠的, 顺序的, 不重复的, 面向连接的通信数据流. 它使用了TCP协议保真了数据传输的正确性.
2) 数据报套接字. 提供一种独立的, 无序的, 不保证可靠的无连接服务. 它使用了UDP协议, 该协议不维护一个连接, 它只把数据打成一个包, 再把远程的IP贴上去, 然后就把这个包发送出去.
3) 原始套接字. 主要应用于底层协议的开发, 进行底层的操作.
TCP协议的基础编程模型
TCP是面向连接的通信协议, 采用客户机-服务器模式, 套接字的全部工作流程如下:
首先, 服务器端启动进程, 调用socket创建一个基于TCP协议的流套接字描述符.
其次, 服务进程调用bind命名套接字, 将套接字描述符绑定到本地地址和本地端口上, 至此socket的半相关描述----{协议, 本地地址, 本地端口}----完成.
再次, 服务器端调用listen, 开始侦听客户端的socket连接请求.
接下来, 客户端创建套接字描述符, 并且调用connect向服务器提交连接请求. 服务器端接收到客户端连接请求后, 调用accept接受, 并创建一个新的套接字描述符与客户端建立连接, 然后原套接字描述符继续侦听客户端的连接请求.
客户端与服务器端的新套接字进行数据传送, 调用write或send向对方发送数据, 调用read或recv接收数据.
在数据交流完毕后, 双方调用close或shutdown关闭套接字.
(1) Socket的创建
在UNIX中使用函数socket创建套接字描述符, 原型如下:
#include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol);
参数type指定了通信类型: SOCK_STREAM, SOCK_DGRAM和SOCK_RAW. 协议AF_INET支持以上三种类型, 而协议AF_UNIX不支持原始套接字.
(2) Socket的命名
函数bind命名一个套接字, 它为该套接字描述符分配一个半相关属性, 原型如下:
#include <sys/types.h> #include <sys/socket.h> int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);
结构sockaddr描述了通用套接字的相关属性, 结构如下
typedef unsigned short int sa_family_t; #define __SOCKADDR_COMMON(sa_prefix) sa_family_t sa_prefix##family struct sockaddr{ __SOCKADDR_COMMON (sa_); /* Common data: address family and length. */ char sa_data[14]; /* Address data. */ };
struct sockaddr_in{ __SOCKADDR_COMMON (sin_); in_port_t sin_port; /* Port number. */ struct in_addr sin_addr; /* Internet address. */ /* Pad to size of `struct sockaddr'. */ unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE - \ sizeof (in_port_t) - sizeof (struct in_addr)]; }; typedef uint32_t in_addr_t; struct in_addr{ in_addr_t s_addr; };
a. IP地址转换
在套接字的协议地址信息结构中, 有一个描述IP地址的整型成员. 我们习惯使用点分方式描述IP地址, 所以需要将其转化为整型数据, 下列函数完成此任务
#include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> int inet_aton(const char *cp, struct in_addr *inp); in_addr_t inet_addr(const char *cp); char *inet_ntoa(struct in_addr in);
b. 字节顺序转换
网络通信常常跨主机, 跨平台, 跨操作系统, 跨硬件设备, 但不同的CPU硬件设备, 不同的操作系统对内存数据的组织结构不尽相同. 在网络通信中, 不同的主机可能采取了不同的记录顺序, 如果不做处理, 通信双方对相同的数据会有不同的解释. 所以需要函数实现主机字节顺序和网络字节顺序的转换
#include <netinet/in.h> uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort);
函数ntohs, ntohl分别将16位和32位的整数从网络字节顺序转换为主机字节顺序.
(3) Socket的侦听
TCP的服务器端必须调用listen才能使套接字进入侦听状态, 原型如下
#include <sys/socket.h> int listen(int s, int backlog);
在TCP通信模型中, 服务器端进程需要完成创建套接字, 命名套接字和侦听接收等一系列操作才能接收客户端连接请求. 下面设计了一个封装了以上三个操作的函数, 代码如下
int CreateSock(int *pSock, int nPort, int nMax){ struct sockaddr_in addrin; struct sockaddr *paddr = (struct sockaddr *)&addrin; int ret = 0; // 保存错误信息 if(!((pSock != NULL) && (nPort > 0) && (nMax > 0))){ printf("input parameter error"); ret = 1; } memset(&addrin, 0, sizeof(addrin)); addrin.sin_family = AF_INET; addrin.sin_addr.s_addr = htonl(INADDR_ANY); addrin.sin_port = htons(nPort); // 创建socket, 在我本机上是5 if((ret == 0) && (*pSock = socket(AF_INET, SOCK_STREAM, 0)) <= 0){ printf("invoke socket error\n"); ret = 1; } // 绑定本地地址 if((ret == 0) && bind(*pSock, paddr, sizeof(addrin)) != 0){ printf("invoke bind error\n"); ret = 1; } if((ret == 0) && listen(*pSock, nMax) != 0){ printf("invoke listen error\n"); ret = 1; } close(*pSock); return(ret); }
服务器端套接字在进入侦听状态后, 通过accept接收客户端的连接请求
#include <sys/types.h> #include <sys/socket.h> int accept(int s, struct sockaddr *addr, socklen_t *addrlen);
下面封装了系统调用accept, 代码如下
#include <fcntl.h> int AcceptSock(int *pSock, int nSock){ struct sockaddr_in addrin; int lSize, flags; if((pSock == NULL) || (nSock <= 0)){ printf("input parameter error!\n"); return 2; } flags = fcntl(nSock, F_GETFL, 0); // 通过fcntl函数确保nSock处于阻塞方式 fcntl(nSock, F_SETFL, flags & ~O_NONBLOCK); while(1){ lSize = sizeof(addrin); memset(&addrin, 0, sizeof(addrin)); // 通过调试, 问题应该出在accept函数 if((*pSock = accept(nSock, (struct sockaddr *)&addrin, &lSize)) > 0) return 0; else if(errno == EINTR) continue; else{ fprintf(stderr, "Error received! No: %d\n", errno); return 1; } } }
套接字可以调用close函数关闭, 也可以调用下面函数
#include <sys/socket.h> int shutdown(int s, int how);
(6) Socket的连接申请
TCP客户端调用connect函数向TCP服务器端发起连接请求, 原型如下
#include <sys/types.h> #include <sys/socket.h> int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
(7) TCP数据的发送和接收
套接字一旦连接上, 就可以发送和接收数据. 原型如下
#include <sys/types.h> #include <sys/socket.h> int send(int s, const void *msg, size_t len, int flags); int recv(int s, void *buf, size_t len, int flags);
如果函数send一次性发送的信息过长, 超过底层协议的最大容量, 就必须分开调用send发送, 否则内核将不予发送信息并且置EMSGSIZE错误.
简单服务器端程序
这里设计了一个TCP服务器端程序的实例, 它创建Socket侦听端口, 与客户端建立连接, 然后接收并打印客户端发送的数据,代码如下
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <stdio.h> #include <errno.h> #define VerifyErr(a, b) \ if (a) { fprintf(stderr, "%s failed.\n", (b)); return 0; } \ else fprintf(stderr, "%s success.\n", (b)); int main(void) { int nSock, nSock1; char buf[2048]; //CreateSock(&nSock, 9001, 9); nSock = nSock1 = 0; // 这里只是为了调试所用 VerifyErr(CreateSock(&nSock, 9001, 9) != 0, "Create Listen SOCKET"); //VerifyErr(AcceptSock(&nSock1, nSock) != 0, "Link"); AcceptSock(&nSock1, nSock); memset(buf, 0, sizeof(buf)); recv(nSock1, buf, sizeof(buf), 0); fprintf(stderr, buf); close(nSock1); close(nSock); return 0; }
[bill@billstone Unix_study]$ make tcp1 cc tcp1.c -o tcp1 [bill@billstone Unix_study]$ ./tcp1 Create Listen SOCKET success. Error received! No: 9 // 出现错误, 为'bad file number'错误 [bill@billstone Unix_study]$