第五篇 网络通信篇
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 #include int socket(int domain, int type, int protocol); |
其中 , 参数 domain 指定发送通信的域 , 有两种选择 : AF_UNIX, 本地主机通信 , 功能和 IPC 对象类似 ; AF_INET, Internet 地址 IPV4 协议 . 在实际编程中 , 我们只使用 AF_INET 协议 , 如果需要与本地主机进程建立连接 , 只需把远程地址设定为 '127.0.0.1' 即可 .
参数 type 指定了通信类型 : SOCK_STREAM, SOCK_DGRAM 和 SOCK_RAW. 协议 AF_INET 支持以上三种类型 , 而协议 AF_UNIX 不支持原始套接字 .
(2) Socket 的命名
函数 bind 命名一个套接字 , 它为该套接字描述符分配一个半相关属性 , 原型如下 :
#include #include int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen); |
参数 s 指定了套接字描述符 , 该值由函数 socket 返回 , 指针 name 指向通用套接字的协议地址结构 , namelen 参数指定了该协议地址结构的长度 .
结构 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. */ }; |
不同的协议有不同的地址描述方式 , 为了便于编码处理 , 每种协议族都定义了自给的套接字地址属性结构 , 协议族 AF_INET 使用结构 sockaddr_in 描述套接字地址信息 , 结构如下 :
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 #include #include 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); |
函数 inet_addr 将参数 ptr 指向的字符串形式 IP 地址转换为 4 字节的整型数据 . 函数 inet_aton 同样完成此功能 . 函数 inet_ntoa 的功能则恰好相反 .
b. 字节顺序转换
网络通信常常跨主机 , 跨平台 , 跨操作系统 , 跨硬件设备 , 但不同的 CPU 硬件设备 , 不同的操作系统对内存数据的组织结构不尽相同 . 在网络通信中 , 不同的主机可能采取了不同的记录顺序 , 如果不做处理 , 通信双方对相同的数据会有不同的解释 . 所以需要函数实现主机字节顺序和网络字节顺序的转换
#include uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort); |
函数 htons, htonl 分别将 16 位和 32 位的整数从主机字节顺序转换为网络字节顺序 .
函数 ntohs, ntohl 分别将 16 位和 32 位的整数从网络字节顺序转换为主机字节顺序 .
(3) Socket 的侦听
TCP 的服务器端必须调用 listen 才能使套接字进入侦听状态 , 原型如下
#include int listen(int s, int backlog); |
参数 s 是调用 socket 创建的套接字 . 参数 backlog 则确定了套接字 s 接收连接的最大数目 .
在 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); } |
(4) Socket 的连接处理
服务器端套接字在进入侦听状态后 , 通过 accept 接收客户端的连接请求
#include #include int accept(int s, struct sockaddr *addr, socklen_t *addrlen); |
函数 accept 一旦调用成功 , 系统将创建一个属性与套接字 s 相同的新的套接字描述符与客户进程通信 , 并返回该新套接字的描述符编号 , 而原套接字 s 仍然用于套接字侦听 . 参数 addr 回传连接成功的客户端地址结构 , 指针 addrlen 回传该结构占用的字节空间大小 .
下面封装了系统调用 accept, 代码如下
#include 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; } } } |
(5) Socket 的关闭
套接字可以调用 close 函数关闭 , 也可以调用下面函数
#include int shutdown(int s, int how); |
函数 shutdown 是强制性地关闭所有套接字连接 , 而函数 close 只将套接字访问计数减 1, 当且仅当计数器值为 0 时 , 系统才真正的关闭套接字通信 .
(6) Socket 的连接申请
TCP 客户端调用 connect 函数向 TCP 服务器端发起连接请求 , 原型如下
#include #include int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen); |
其中 , serv_addr 指针指定了对方的套接字地址结构 .
(7) TCP 数据的发送和接收
套接字一旦连接上 , 就可以发送和接收数据 . 原型如下
#include #include int send(int s, const void *msg, size_t len, int flags); int recv(int s, void *buf, size_t len, int flags); |
函数 send(recv) 应用于 TCP 协议的套接字通信中 , s 是与远程地址连接的套接字描述符 , 指针 msg 指向待发送的数据信息 ( 或接收数据的缓冲区 ), 此信息共 len 个字节 ( 或最大可接收 len 个字节 ).
如果函数 send 一次性发送的信息过长 , 超过底层协议的最大容量 , 就必须分开调用 send 发送 , 否则内核将不予发送信息并且置 EMSGSIZE 错误 .
简单服务器端程序
这里设计了一个 TCP 服务器端程序的实例 , 它创建 Socket 侦听端口 , 与客户端建立连接 , 然后接收并打印客户端发送的数据 , 代码如下
#include #include #include #include #include
#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; } |
运行程序 , 并在在浏览器中输入 http://127.0.0.1: 9001/, 你将得到一条来自客户端的 http 报文 . 但是在这里却出现了问题 , 问题如下 :
[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]$ |