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

 

 

第五篇 网络通信篇

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

  其中 , 参数 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 <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]$

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值