学习笔记11-学习《精通UNIX下C语言编程及项目实践》

来源:互联网 发布:淘宝卖家搞笑回复 编辑:程序博客网 时间:2024/06/12 07:18

 

 

第五篇 网络通信篇

  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接受, 并创建一个新的套接字描述符与客户端建立连接, 然后原套接字描述符继续侦听客户端的连接请求.

  客户端与服务器端的新套接字进行数据传送, 调用writesend向对方发送数据, 调用readrecv接收数据.

  在数据交流完毕后, 双方调用closeshutdown关闭套接字.

  (1) Socket的创建

  UNIX中使用函数socket创建套接字描述符, 原型如下:

#include <sys/types.h>

#include <sys/socket.h>

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_DGRAMSOCK_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);

  参数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 <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);

  函数inet_addr将参数ptr指向的字符串形式IP地址转换为4字节的整型数据. 函数inet_aton同样完成此功能. 函数inet_ntoa的功能则恰好相反.

  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);

  函数htons, htonl分别将16位和32位的整数从主机字节顺序转换为网络字节顺序.

  函数ntohs, ntohl分别将16位和32位的整数从网络字节顺序转换为主机字节顺序.

  (3) Socket的侦听

  TCP的服务器端必须调用listen才能使套接字进入侦听状态, 原型如下

#include <sys/socket.h>

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 <sys/types.h>

#include <sys/socket.h>

int accept(int s, struct sockaddr *addr, socklen_t *addrlen);

  函数accept一旦调用成功, 系统将创建一个属性与套接字s相同的新的套接字描述符与客户进程通信, 并返回该新套接字的描述符编号, 而原套接字s仍然用于套接字侦听. 参数addr回传连接成功的客户端地址结构, 指针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;

                 }

        }

}

  (5) Socket的关闭

 套接字可以调用close函数关闭, 也可以调用下面函数

#include <sys/socket.h>

int shutdown(int s, int how);

  函数shutdown是强制性地关闭所有套接字连接, 而函数close只将套接字访问计数减1, 当且仅当计数器值为0, 系统才真正的关闭套接字通信.

  (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);

  其中, serv_addr指针指定了对方的套接字地址结构.

  (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(recv)应用于TCP协议的套接字通信中, s是与远程地址连接的套接字描述符, 指针msg指向待发送的数据信息(或接收数据的缓冲区), 此信息共len个字节(或最大可接收len个字节).

  如果函数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;

}

  运行程序, 并在在浏览器中输入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]$