Socket编程

本文介绍了socket的概念及其在网络通讯中的作用,对比了流式套接口和数据报套接口的特点,并讲解了socket编程中的关键数据结构和函数,如struct sockaddr_in、inet_addr()及gethostbyname()等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 
什么是socket
socket是使用 Unix 文件描述符 (fiel descriptor) 和其他程序通讯的方式。Unix 程序在执行任何形式的 I/O时, 程序是在读或者写一个文件描述符。一个文件描述符只是一个和打开的文件相关联的整数。 但是,这个文件可能是一个网络连接,FIFO,管道,终端,磁盘上的文件或者什么其他的东西。Unix 中所有的东西是文件。因此,想和 Internet 上别 的程序通讯的时候,将要通过文件描述符。
从哪里得到网络通讯的文件描述符呢,利用系统调用 socket()。它返回套接口描述符(socket descriptor),然后你再通过它来调用 send()recv()。如果它是个文件描述符,那么为什么不用一般的调用 read()write() 来通过套接口通讯。答案是可以,但是使用 send()recv() 让程序员更好的控制数据传输。
在我们的世界上,有很多种套接口。有 DARPA Internet 地址 (Internet 套接口),本地节点的路径名 (Unix 套接口),CCITT X.25 地址 (可以完全忽略 X.25 套接口)。 也许在Unix 机器上还有其他的。这里只讲第一种:Internet 套接口。
 
Internet 套接口的两种类型
一种是 "Stream Sockets",另外一种是 "Datagram Sockets"。数据报套接口有时也叫无连接套接口。流式套接口是可靠的双向通讯的数据流。如果向套接口安顺序输出“1,2”,那么它们将安顺序“1,2”到达另一边。它们也是无错误的传递的,有自己的错误控制。
 
telnet就使用流式套接口。WWW 浏览器使用的 HTTP 协议也使用他们。流式套接口可以达到高质量的数据传输,它使用了“传输控制协议 (The Transmission Control Protocol)”,TCP 控制数据按顺序到达并且没有错误。TCP/IP中IP 是指 “Internet 协议”, IP 只是处理 Internet 路由而已。
 
数据报套接口为什么不可靠。如果发送一个数据报,它可能到达,它可能次序颠倒了。如果它到达,那么在这个包的内部是无错误的。数据报也使用 IP 作路由,但是不选择 TCP。它使用“用户数据报协议 (User Datagram Protocol)”。为什么是无连接,主要原因是因为它并不象流式套接口那样维持一个连接。只要建立一个包,在目标信息中构造一个 IP 头,然后发出去。不需要连接。应用程序有: tftp, bootp 等等。
如果数据丢失了这些程序如何正常工作。每个程序在 UDP 上有自己的协议。例如,ftp 协议每发出一个包,收到者发回一个包来说“我收到了!” (一个“命令正确应答”也叫“ACK” 包)。如果在一定时间内,发送方没有收到应答, 它将重新发送,直到得到 ACK。这一点在实现 SOCK_DGRAM 应用程序的时候非常重要。
 
网络理论
 数据封装 (Data Encapsulation)主要的内容是:一个包,先是被第一个协议包装(“封装”), 然后,整个数据被另外一个协议封装,然后下一个,一直重复下去,直到硬件(物理)层( Ethernet )。
 网络分层模型 (Layered Network Model)。 这种网络模型在描述网络系统上相对其他模型有很多优点。例如,写一个套接口,程序而不用关心数据的物理传输(串行口,以太网,连接单元接口 (AUI) 还是其他介质)。 因为底层的程序为我们处理。实际的网络硬件和拓扑对于程序员来说是透明的。
整个层次模型:
应用层 (Application)
表示层 (Presentation)
会话层 (Session)
传输层 (Transport)
网络层 (Network)
数据链路层 (Data Link)
物理层 (Physical)
物理层是硬件(串口,以太网等等)。应用层是和硬件层相隔最远的—它是用户和网络交互的地方。
把它应用到 Unix,结果是:
应用层 (Application Layer) (telnet, ftp, 等等)
传输层 (Host-to-Host Transport Layer) (TCP, UDP)
Internet 层 (Internet Layer) (IP 和路由)
网络访问层 (Network Access Layer) (网络层,数据链路层和物理层)
现在,可能看到这些层次如何协调来封装原始的数据了。
 
struct
套接口用到的各种数据类型。
首先是简单的一个:socket descriptor。它是int类型,仅仅是一个常见的 int
两种字节排列顺序:重要的字节在前面(有时叫 "octet"),或者不重要的字节在前面。 前一种叫“网络字节顺序 (Network Byte Order)”。有些机器在内部是按照这个顺序储存数据,而另外一些则不然。当某数据必须按照 NBO 顺序,那么要调用函数( htons() )来将它从本机字节顺序(Host Byte Order) 转换过来。如果没有提到 NBO, 那么就让它是本机字节顺序。
struct sockaddr. 这个数据结构为许多类型的套接口储存套接口地址信息:
    struct sockaddr {
        unsigned short    sa_family;    /* address family, AF_xxx       */
        char              sa_data[14]; /* 14 bytes of protocol address */
    };
sa_family 能够是各种各样的事情,但是在这是 "AF_INET"。 sa_data 为套接口储存目标地址和端口信息。为了对付struct sockaddr,程序员创造了一个并列的结构:struct sockaddr_in (in代表Internet)
    struct sockaddr_in {
        short int          sin_family; /* Address family               */
        unsigned short int sin_port;    /* Port number                  */
        struct in_addr     sin_addr;    /* Internet address             */
        unsigned char      sin_zero[8]; /* Same size as struct sockaddr */
    };
这个数据结构让可以轻松处理套接口地址的基本元素。注意 sin_zero (它被加入到这个结构,并且长度和 struct sockaddr 一样) 应该使用函数 bzero()memset() 来全部置零。这样,即使 socket() 想要的是 struct sockaddr *, 仍然可以使用 struct sockaddr_in。注意 sin_familysa_family 一致并能够设置为 "AF_INET"。最后, sin_portsin_addr 必须是网络字节顺序 (Network Byte Order)。
再看这个数据结构: struct in_addr, 有这样一个联合 (unions):
    /* Internet address (a structure for historical reasons) */
    struct in_addr {
        unsigned long s_addr;
    };
如果声明 "ina" 是 数据结构 struct sockaddr_in 的实例,那么 "ina.sin_addr.s_addr" 就储存4字节的 IP 地址(网络字节顺序)。如果你系统使用的还是联合 struct in_addr ,还是可以放心4字节的 IP 地址是和上面说的一样(这是因为 #define。)
 
Convert the Natives
网络字节顺序和本机字节顺序之间的转换。能够转换两种类型: short (两个字节)和 long (四个字节)。这个函数对于变量类型 unsigned 也适用。假设想将short 从本机字节顺序转换为网络字节顺序。用 "h" 表示 "本机 (host)",接着是 "to",然后用 "n" 表示 "网络 (network)",最后用 "s" 表示 "short": h-to-n-s, 或者 htons() ("Host to Network Short")。
htons()--"Host to Network Short"
htonl()--"Host to Network Long"
ntohs()--"Network to Host Short"
ntohl()--"Network to Host Long"
记住:在将数据放到网络上的时候,确信它们是网络字节顺序。
为什么struct sockaddr_in数据结构中, sin_addrsin_port 需要转换为网络字节顺序,而 sin_family 不需要呢? 答案是:sin_addrsin_port 分别封装在包的 IP 和 UDP 层。因此,他们必须要是网络字节顺序。 但是 sin_family 域只是被内核 (kernel) 使用来决定在数据结构中包含什么 类型的地址,所以它应该是本机字节顺序。也即 sin_family 没有发送到网络上,它们是本机字节顺序。
 
IP 地址
首先,假设用 struct sockaddr_in ina,想将 IP 地址 "132.241.5.10" 储存到其中。要用的函数是 inet_addr(),转换 numbers-and-dots 格式的 IP 地址到 unsigned long。这个工作可以这样来做:
    ina.sin_addr.s_addr = inet_addr("132.241.5.10");
注意:inet_addr() 返回的地址已经是按照网络字节顺序的,没有必要再去调用 htonl()
上面的代码可不是很健壮 (robust),因为没有错误检查。inet_addr() 在发生错误 的时候返回-1。记得二进制数,在IP 地址为 255.255.255.255 的时候返回的是 (unsigned)-1。这是个广播地址。记住正确的使用错误检查。
数据结构 struct in_addr如何按照 numbers-and-dots 格式打印。在这个时候,要用函数 inet_ntoa() ("ntoa" 意思是 "network to ascii"):
    printf("%s",inet_ntoa(ina.sin_addr));
它将打印 IP 地址。注意的是:函数 inet_ntoa() 的参数是 struct in_addr,而不是 long。同时要注意的是它返回的是一个指向字符的指针。 在 inet_ntoa 内部的指针静态地储存字符数组,因此每次调用 inet_ntoa() 的时候它将覆盖以前的内容。例如:
    char *a1, *a2;
    ……
    a1 = inet_ntoa(ina1.sin_addr); /* this is 198.92.129.1 */
    a2 = inet_ntoa(ina2.sin_addr); /* this is 132.241.5.10 */
    printf("address 1: %s/n",a1);
    printf("address 2: %s/n",a2);
运行结果是:
    address 1: 132.241.5.10
    address 2: 132.241.5.10
 
DNS
DNS代表"域名服务 (Domain Name Service)"。主要的功能是:我们给它一个容易记忆的某站点的地址,它给我们IP 地址(然后就可以使用 bind(),connect(),sendto() 或者其他函数。)当一个人输入:
    telnet whitehouse.gov
telnet 能知道它将连接 (connect()) 到 "198.137.240.100"。
但是这是如何工作的呢?可以调用函数 gethostbyname()
    #include <netdb.h>
   
    struct hostent *gethostbyname(const char *name);
它返回一个指向 struct hostent 的指针。这个数据结构是这样的:
    struct hostent {
        char    *h_name;
        char    **h_aliases;
        int     h_addrtype;
        int     h_length;
        char    **h_addr_list;
    };
    #define h_addr h_addr_list[0]
这里是这个数据结构的详细资料: struct hostent:
h_name - Official name of the host.
h_aliases - A NULL-terminated array of alternate names for the host.
h_addrtype - The type of address being returned; usually AF_INET.
h_length - The length of the address in bytes.
h_addr_list - A zero-terminated array of network addresses for the host. Host addresses are in Network Byte Order.
h_addr - The first address in h_addr_list.
gethostbyname() 成功时返回一个指向 struct hostent 的 指针,或者是个空 (NULL) 指针。(但是和以前不同,errno 不设置,h_errno 设置错误信息。请看下面的 herror()。)
但是如何使用呢? 这里是个例子:
    #include <stdio.h>
    #include <stdlib.h>
    #include <errno.h>
    #include <netdb.h>
    #include <sys/types.h>
    #include <netinet/in.h>
 
    int main(int argc, char *argv[])
    {
        struct hostent *h;
 
        if (argc != 2)
{ /* error check the command line */
            fprintf(stderr,"usage: getip address/n");
            exit(1);
        }
 
        if ((h=gethostbyname(argv[1])) == NULL) { /* get the host info */
            herror("gethostbyname");
            exit(1);
        }
 
        printf("Host name : %s/n", h->h_name);
        printf("IP Address : %s/n",inet_ntoa(*((struct in_addr *)h->h_addr)));
 
        return 0;
    }
在使用 gethostbyname() 的时候,不能用 perror() 打印错误信息(因 为 errno 没有使用),应该调用 herror()
相当简单,只是传递一个保存机器名的字符串(例如 "whitehouse.gov") 给 gethostbyname(),然后从返回的数据结构 struct hostent 中 收集信息。
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值