C语言基于TCP/IP的socket编程

有时候我们在进行网络编程的时候,不得不和socket打交道,以前一遇到socket就很畏惧,这次下定决心来研究一下 这一块内容,选什么语言呢?java不行,它封装的太好,于了解底层的细节没有帮助,还是去看C语言的实现才能看得更 透彻。之前跟项目组长进行交流,他说学好了c语言和UNIX环境编程,底层的一些概念和理解就不成问题了,事后想了 想这真是这么回事儿,现在就来聊一聊C语言的socket编程。

Socket套接字

说到socket套接字,可能觉得很泛,为了具有普适性,我尽量从TCP和UDP来说说套接字的底层实现。

  • 套接字的结构如何?

     int sock = socket(AF_INEF,SOCK_STREAM,IPPROTO_TCP);
    

上面的代码创建了一个socket套接字,AF_INEF表示建立的是IPV4套接字,后面两个参数表示使用的是基于TCP的 流的协议。最关键的是这个返回值sock,这可不是普通的返回码,而是系统创建的指向socket数据结构的描述符, 它是整数型的,与文件描述符的概念相同。那么套接字结构里的内容是什么呢?

     1. 与套接字关联的本地和远程Internet地址以及端口号。
     2. 一个等待递送所接受数据的FIFO队列("RecvQ")和一个等待传输的数据的FIFO队列("SendQ")。
     3. 对于TCP连接,有相关的打开和关闭TCP握手的协议状态信息,初始值为Closed。

对于UDP的套接字,比TCP的要简单,就是没有连接的状态信息,我们最常使用的netstat命令就是对这个底层
数据结构的快照。进程对套接字的处理都是通过sock这个“句柄”来完成的,由于进程对于文件描述符的个数是有
限制的,所以一个进程能建立的socket连接也是有限的。
  • socket的地址结构是怎样的?

     struct sockaddr_in {
        sa_family_t  sin_family;  //网络协议(AF_INEF)
        in_port_t    sin_port;    //端口(16位无符号整数)
        struct in_addr   sin_addr;  //IPV4地址(32位,如果是ipv6:128位)
        char sin_zero[8];           //字符填充
     }
    

    后面的注释已经对socket的地址信息解释的很清楚了,对于tcp连接,server端需要将监听的socket与服务器的 地址进行绑定,这样socket才能正常监听客户端的连接,这个动作就是绑定:

     if(bind(servsock,(struct sockaddr*)&servAddr, sizeof(servAddr) < 0){
        puts("bind() failed");
     }
    

    对于TCP我们在bind完之后需要进行Listen监听和阻塞等待accept,两个函数如下:

     int listen(int socket,int queueLimit)
     int accept(int socket,struct sockaddr *clientAddress,socklen_t *addressLength)
    

    但是对于UDP这两部显然是多余的,只要给UDPbind上地址,它就可以开始接受UDP包了。

  • TCP和UDP在接收和发送消息时有何区别?

    对于TCP: ssize_t numBytes = send(sock,echoString,echoStringLen,0); numBytes = recv(sock,buffer,BUFSIZE-1,0);

    对于UDP: ssize_t sendto(int,sock,const void msg,size_t msgLength,int flags,const structsockaddr destAddr,socketlen_t addrlen);

     ssize_t recvfrom(int,sock,const void *msg,size_t msgLength,int flags,const structsockaddr *destAddr,socketlen_t addrlen);
    

    从上面的代码中就可以看出,面向连接的TCP不需要在send中指定目的地,因为TCP的socket本身就是成对存在的。

  • 在socket中怎么使用名称(DNS)?

    我们在编码的时候并不总是知道服务器的具体IP地址,我们可能只知道域名,而且IP地址难以记忆且可能经常 改变,所以在socket中使用DNS是必须要有的。

      int getaddrinfo(const char *hostStr,const char *serviceStr,const sruct addrinfo *hints,struct addrinfo **results)
      int getnameinfo(const struct sockaddr *address,socklen_t addressLength,char *node,socklen_t nodeLength,char *service,socklen_t serviceLength,int flags)
    

    第一个函数就是解析域名的,返回的结果是指向addrinfo结构的链表的指针。为什么要返回一个链表结果呢?因为 对于主机和服务的每种组合,可能有地址族(V4或者V6)和套接字类型/协议(流/TCP或数据包/UDP)的多种不同的 组合表示可能的断点;再者主机名可能对应多个IP地址。当然我们也不用去考虑太复杂,因为第三个参数的地址 格式会为我们过滤地址格式,这个函数的功能实在强大。

    第二个函数就是一个“逆函数”,它通过地址信息返回解析的对应域名。

流与Socket

首先说明流只能用于TCP套接字。

     FILE *fdopen(int socketdes, const char *mode);
     size_t fwrite(const void *ptr,size_t size,size_t nmemb,FILE *stream);
     size_t fread(void *ptr,size_t size,size_t nmemb,FILE *stream);

从第一个函数可以看出socket的句柄居然可以使用流打开,这也应征了一个结论就是套接字的句柄跟文件描述符
没有本质上的区别。通过这种方式,我们就可以轻松地将结构写入流中,也可以从流中读出一个数据结构,而且
fread永远不会从流中读取对象的一部分,每次都是读取给定大小的给定数量的对象,fwrite也是一样。这样相当
与将一个结构覆盖在一片内存区域上,这就需要字节的对齐和填充(一般是自动完成)。这样是不是方便很多。

信号与socket

 因为应用程序接收到大部分信号采取的默认行为都是终止进程,所以如果编写服务端的进程不进行信号的处理,
 很容易因为意外的错误而终止服务进程,这是我们不愿意看到的,所以为了服务进程的健壮性,我们不得不
 对信号进行处理。

 所以在TCP套接字上发送数据的任何程序都必须显式地处理SIGPIPE信号。

 为什么呢?考虑这样的情况:在TCP中,如果客户端突然的关闭连接,而服务器可能不知道这个信息,当服务
 端去发送数据的时候,这个时候服务器就会知晓这个连接已经断开,就会递送一个SIGPIPE信号,导致服务进程
 的退出,所以处理SIGPIPE是必要的,一般的做法是将SIGPIPE设置成SIG_IGN,这样就可以忽略该信号。

 我们都知道kill命令是专门向进程发送信号的,进程是不能忽略SIGKILL信号(9号)的,所以我们总能够通过
 kill -9 ${PID}来杀死进程。
  • 何为异步IO?

    在编写阻塞IO程序的时候,我们是为每一个客户端编写一个进程或者线程来处理,这样的开销可想而知,服务器 不可能承受太多的连接数,这样我们想到了非阻塞的IO,用一个进程或线程去轮询IO事件,这样是不是会浪费 很多的CPU时间,我们能不能让操作系统通知进程套接字的IO事件呢?答案是肯定的。

    异步IO的工作方式是:当套接字上发生某个与IO相关的事件时,把SIGIO信号递送给进程,然后进程再去处理 IO事件,大名鼎鼎的select/Epoll模型就是使用的异步非阻塞的IO模型,通常epoll的处理效率会更高,因为 select在接收到信号后还是得去轮询IO描述符,但是epoll会告诉进程确切的信息。

  • 如何实现超时处理?

    最典型的例子就是:UDP消息可能丢失,而客户不能辨别是否发生了丢失,当然客户端也不可能永远等待,这 就需要一个超时机制,譬如当2秒钟内还没有收到回应,就认为服务不可用,或者重新尝试一下。怎么实现呢?

    标准方法是在阻塞函数之前设置一个alarm:

      unsigned int alarm(unsigned int secs);
    

    当计时器到期的时候,就把SIGALRM信号发送给进程,并且执行用于SIGALRM的处理函数,如果接收到相应的消息, 就将计时器取消。

通过信号,我们还可以收回僵尸进程,当子进程终止时,它不会自动消失,而是变成了僵尸进程,会消耗系统的 资源,这样我们可以通过SIGCHLD信号来收回僵尸进程(waitpid),因为子进程终止的时候会发送SIGCHLD信号给 父进程。

通过信号可以做的事情真是太美妙了。



Previous     Next
zhing /
Published under (CC) BY-NC-SA in categories C语言  tagged with C语言