了解套接字Socket



了解套接字Socket

 

套接字是通过操作系统(OS)完成网络通信的一种标准方法。可以将套接字看作是与连接相连的一个终端,就像是操作员配电盘上的一个插座一样。但是这些套接字只是程序员的抽象称呼,它们负责有文描述的OSI模型的所有基本细节。对程序员来说,可以使用一个套接字通过网络发送或接收数据。这些数据在较低的层(由操作系统处理)之上的会话层(5)传输,该层负责路由。有几种不同的套接字,它们决定了传输层的结构。最常见的类型是流套接字和数据报套接字。

流套接字提供了可靠的双向通信,这类似于您和他人打电话。一方向另一方发起连接,建立连接之后,任何一方都可以和另一方通信。此外,您所说的话实际上是否到达目的地能够得到快速证实。流套接字使用一种称为传输控制协议(Transmission Control ProtocolTCP)的标准通信协议,这个协议存在于OSI模型的传输层(4)。在计算机网络上,数据通常以我们称之为包的大数据块的形式传输。TCP被设计为数据包按顺序到达目的地并且无差错,就像在电话中讲话时,单词以它们被说出的顺序到达另一端一样。Web服务器、邮件服务器以及它们各自的客户应用程序都使用TCP和流套接字进行通信。

另一种常见的套接字类型是数据报套接字。使用数据报套接字通信更像是邮寄一封信而不是打电话。连接是单向的并且不可靠。如果您寄了几封信,您将不能确定它们是否按照和邮寄时相同的顺序到达目的地,甚至连能否被送达目的地也不能保证。邮政服务相当可靠,但Intemet并不可靠。数据报套接字在传输层(4)上使用另一种称为UDP的标准协议来代替TCPUDP代表用户数据报协议(User DatagramProtocol),意味着可以用它来创建自定义协议。这个协议非常基本并且是轻量级的,它只内置了很少的保护措施。它并不是一种真正的连接,只是一种从一端向另一端发送数据的基本方法。使用数据报套接字时,协议中的系统开销非常少,但协议完成的功能也不多。如果程序需要证实另一方接收到了数据包,必须编程使另一方回送一个确认包。有些情况下,可以接受数据包的丢失。

数据报套接字和UDP普遍用于网络游戏和流媒体,因为开发人员可以根据需要精确地修整他们的通信,而不会存在像TCP那样的固有系统开销。

一、   套接字函数

C语言中,套接字的行为类似于文件,因为它们使用文件描述符来标识它们自己。套接字的行为与文件非常相似,实际上利用套接字文件描述符,可以使用read()write()函数接收和发送数据。但是,有几个函数是专门设计用来处理套接字的。在/usr/include/sys/socket.h文件中有这些函数原型的定义。

extern intsocket (int __domain, int __type, int __protocol) __THROW;

用于创建一个新套接字,返回一个表牙示套接字后的文件描述符,错误时遣返回-1

extern intconnect (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);

将一个套接字(由文件描述符fd指定)连接到远程主机。成功返回0,错误返回-1

Listen(int fd,int backlog_queue_size)

侦听传入的连接并将连接请求排队,:直到数量达到backlog_queue_size。成功返回0,错误返回-1

extern int accept (int __fd, __SOCKADDR_ARG __addr,socklen_t *__restrict __addr_len);

一个绑定的端口上接受一个传入连接。远程主机的地址信息写入remote_host结构中,地址结构的实际大小写入到addr_len中。这个函数返回一个新套接字文件描述符来标识已经连接的套接字,错误返回-1

extern ssize_tsend (int __fd, __const void *__buf, size_t __n, int __flags);

*__buf向套接字fd发送n个字节,返回值为发送的字节数,错误返回-1

extern ssize_trecv (int __fd, void *__buf, size_t __n, int __flags);

从套接字fd接收n个字节到*__buf中,返回值为收到的字节数,错误返回-1

使用socket()函数创建套接字时,必须指定套接字的域(domain)、类型(type)和切协议( protocol)。域指的是套接字的协议族。套接字可以使用各种协议进行通信,从浏览Web时使用的标准Internet协议到诸如AX.25,这样的业余无线电协议(如果您是一个无线电发烧友)。这些协议族在bits/socket.h中定义,它自动包含在sys/socket.h中。

/usr/include/bits/socket.h片段1

/* Protocolfamilies. */

#definePF_UNSPEC  0   /* Unspecified. */

#definePF_LOCAL   1   /* Local to host (pipes andfile-domain). */

#definePF_UNIX    PF_LOCAL/* Old BSD name for PF_LOCAL. */

#definePF_FILE    PF_LOCAL/* Another non-standard name for PF_LOCAL. */

#definePF_INET    2   /* IP protocol family. */

#definePF_AX25    3   /* Amateur Radio AX.25. */

#definePF_IPX     4   /* Novell Internet Protocol. */

#definePF_APPLETALK   5   /* Appletalk DDP. */

#definePF_NETROM  6   /* Amateur radio NetROM. */

#definePF_BRIDGE  7   /* Multiprotocol bridge. */

#definePF_ATMPVC  8   /* ATM PVCs. */

#definePF_X25     9   /* Reserved for X.25 project. */

#definePF_INET6   10  /* IP version 6. */

如前所述,虽然流套接霉字和数据报套接字最常使用,但还有其他几种类型的套接字。套接字的类型也定义在bits/socket.h中(上面代码中的/*comment*/是另一种形式的注释,它将所有处于星号之间的内容作为注释)。

/usr/include/bits/socket.h片段2

/* Types ofsockets. */

enum__socket_type

{

 SOCK_STREAM = 1,     /* Sequenced, reliable, connection-based

                 byte streams.  */

#defineSOCK_STREAM SOCK_STREAM

 SOCK_DGRAM = 2,      /* Connectionless, unreliable datagrams

                 of fixed maximum length. */

#defineSOCK_DGRAM SOCK_DGRAM

 SOCK_RAW = 3,        /*Raw protocol interface. */

#defineSOCK_RAW SOCK_RAW

 SOCK_RDM = 4,        /*Reliably-delivered messages. */

#defineSOCK_RDM SOCK_RDM

 SOCK_SEQPACKET = 5,      /* Sequenced, reliable, connection-based,

                 datagrams of fixed maximum length. */

#defineSOCK_SEQPACKET SOCK_SEQPACKET

 SOCK_PACKET = 10     /* Linux specific way of getting packets

                 at the dev level.  For writing rarp and

                 other similar things on the user level. */

#defineSOCK_PACKET SOCK_PACKET

};

Socket()函数的最后一个参数是协议,该参数通常为0。该函数的详细说明书允许使用一个协议族中的多个协议,因此这个参数用来从协议族中选择一个协议。但是,实际上大多数协议族中仅有一个协议,这意味着这个参数通常被设置为0,即选用协议族列表中第一个也是唯一的一个协议。本书使用套接字所做的事情也是这种情况,所以在我们的例子中这个参数总是O

二、 套接字地址

许多套接字函数引用一个sockad出结构来传递定义了一个主机的地址信息。这个结构也定义在bits/socket.h中,如后面所示。

/usr/include/bits/socket.h片段3

/* Get thedefinition of the macro to define the common sockaddr members. */

#include<bits/sockaddr.h>

 

/* Structuredescribing a generic socket address. */

structsockaddr

 {

   __SOCKADDR_COMMON (sa_);/* Common data: address family and length. */

   char sa_data[14];       /* Address data. */

 };

SOCKADDR_COMMON在文件bits/sockaddr.h中定义,其本质是将参数转换为一个无符号短整型数。这个值定义了地址的地址族,结构的其余部分用于保存地址数据。因为套接字可以使用各种协议族进行通信,根据地址族的不同,每个协议族都有自己的定义终端地址的方法,所以必须将地址定义为变量。可用的地址族也在bits/socket.h中定义,它们通常直接转换成相应的协议族。

/usr/include/bits/socket.h 片段4

/* Addressfamilies. */

#defineAF_UNSPEC  PF_UNSPEC

#defineAF_LOCAL   PF_LOCAL

#defineAF_UNIX    PF_UNIX

#defineAF_FILE    PF_FILE

#defineAF_INET    PF_INET

#defineAF_AX25    PF_AX25

#defineAF_IPX     PF_IPX

#defineAF_APPLETALK   PF_APPLETALK

#defineAF_NETROM  PF_NETROM

#defineAF_BRIDGE  PF_BRIDGE

#defineAF_ATMPVC  PF_ATMPVC

#defineAF_X25     PF_X25

#defineAF_INET6   PF_INET6

#defineAF_ROSE    PF_ROSE

#defineAF_DECnet  PF_DECnet

#defineAF_NETBEUI PF_NETBEUI

#defineAF_SECURITYPF_SECURITY

#defineAF_KEY     PF_KEY

#defineAF_NETLINK PF_NETLINK

#defineAF_ROUTE   PF_ROUTE

#defineAF_PACKET  PF_PACKET

#defineAF_ASH     PF_ASH

#defineAF_ECONET  PF_ECONET

#defineAF_ATMSVC  PF_ATMSVC

#defineAF_SNA     PF_SNA

#defineAF_IRDA    PF_IRDA

#defineAF_PPPOX   PF_PPPOX

#defineAF_WANPIPE PF_WANPIPE

#defineAF_BLUETOOTH   PF_BLUETOOTH

#defineAF_MAX     PF_MAX

因为地址可以包含不同类型的信息(这取决于地址族),所以有其他几种地址结构,它们在地址数据部分包含了来自于sockaddr结构的公共元素以及地址族的特殊信息。这些结构大小相同,因此可以将它们从一种类型强制转换到另一种类型。这意味着socket()函数会简单地接受一个指向sockaddr结构的指针,而这个指针事实上可以指向一个IPv4IPv6X.25地址。这样就允许套接字函数操作各种协议了。

本书将使用地址族AF_INET处理网际协议第4(IPv4),即协议族PF_INET。文件netinet/in.h中定义了AF_INET的并行套接字地址结构。

/usr/include/netinet/in.h片段5

/* Structuredescribing an Internet socket address. */

structsockaddr_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 (structsockaddr) -

             __SOCKADDR_COMMON_SIZE -

             sizeof (in_port_t) -

             sizeof (struct in_addr)];

 };

上面结构中的SOCKADDR_COMMON部分就是前面提到的无符号整型数,它用来定义地址族。因为一个套接字终端地址包含一个Internet地址和一个端口号,所以结构中接下来就是这两个值。端口号是一个16位短整型数,而用于Internet地址的in_addr结构包含一个32位数。结构的其余部分恰好是8个字节填充空间,它用于填充sockaddr结构的其余部分。任何数据都不会使用这个空间,但必须保留它,这样可以将结构相互强制转换。最终,套接字地址结构如图4-2所示一样结束。

 

三、 网络字节顺序

AF_INET套接字地址结构中使用的端口号和IP地址期望遵循网络字节顺序,即big-endian。这与x86little-endian字节顺序正好相反,因此必须将这些数值转换。有几个专门的函数用于这些转换,它们的原型在包含文件netinet/in.harpa /inet.h中定义。这里是这些常见字节顺序转换函数的汇总:

  • htonl (long类型的值)--主机到网络的long类型:将32位整数从主机的字节顺序转换成网络字节顺序。

  • htonsshort类型的值)--主机到网络的short类型:将16位整数从主机的字节顺序转换成网络字节顺序。

  • ntohllong类型的值)--网络到主机的long类型:将32位整数从网络字节顺序转换成主机的字节顺序。

  • ntohsshort类型的值)--网络到主机的short类型:将16位整数从网络字节顺序转换成主机的字节顺序。

    为了与所有结构兼容,即使主机使用一个具有big-endian字节顺序的处理器,也仍然使用这些转换函数。

    四、 Internet地址转换

    看到数字12.110.110.204时,您也许会认出这是一个Internet地址(IPv4)。这种熟悉的表示法是一种常见的表示Internet地址的方法,有一些函数可以将这种表示法与32位整数按照网络字节顺序进行相互转换。这些函数在包含文件arpa/inet.h中定义,其中两个最有用的转换函数是:

    • inet_aton(char *ascii_addr,struct in_addr *network _addr)(ASCII到网络)

      这个函数将一个包含点分十进制格式IP地址的ASCII字符串转换成一个in_addr结构,正如您记忆中的那样,它只包含一个以网络字节顺序表示IP地址的32位整数。

    • inet_ntoa(struct in_addr*network_addr)(网络到ASCII)

      这个函数的转换正好相反。它传递一个指向in_addr结构的指针,该结构中包含IP地址,返回一个指向ASCII字符串的指针,该字符串中包含以点分十进制格式表示的IP地址。这个字符串保存在函数中一个静态的己分配缓存区中,因此在下一次调用inet_ntoa()将字符串覆盖之前,都可以对它进行访问。

      五、 一个简单的服务器示例

      示范如何使用这些函数的最好方法是通过示例。下面的服务器代码在端口7890侦听TCP连接。客户端连接时,它会发送信息“Helloworld哆然后接收数据,直到连接关闭。这是通过使用套接字函数和前面提到的包含文件中的结构实现的,因此要在程序开始时包含这些文件。程序已经向hacking.h中添加了一个有用的内存转储函数,下面的代码中显示了这个文件。

      添加到hacking.h

      // dumps rawmemory in hex byte and printable split format

      voiddump(const unsigned char *data_buffer, const unsigned int length) {

         unsigned char byte;

         unsigned int i, j;

         for(i=0; i < length; i++) {

             byte = data_buffer[i];

             printf("%02x ",data_buffer[i]); // display byte in hex

             if(((i%16)==15) || (i==length-1)) {

                 for(j=0; j < 15-(i%16); j++)

                     printf("  ");

                 printf("| ");

                 for(j=(i-(i%16)); j <= i; j++){ // display printable bytes from line

                     byte = data_buffer[j];

                     if((byte > 31) &&(byte < 127)) // outside printable char range

                         printf("%c",byte);

                     else

                         printf(".");

                 }

                 printf("\n"); // end ofthe dump line (each line 16 bytes)

             } // end if

         } // end for

      }

       

      服务器程序使用这个函数显示包数据。但是,因为它在其他地方也有用处,所以已经将它添加到了hacking.h文件中。服务器程序的其余部分会在阅读源代码时进行解释。

      Simple_server.c

      #include<stdio.h>

      #include<stdlib.h>

      #include <string.h>

      #include<sys/socket.h>

      #include<netinet/in.h>

      #include<arpa/inet.h>

      #include"hacking.h"

      #define PORT7890  // the port users will beconnecting to

      int main(void){

         int sockfd, new_sockfd; // listen on sock_fd, new connection onnew_fd

         struct sockaddr_in host_addr, client_addr; // my address information

         socklen_t sin_size;

         int recv_length=1, yes=1;

         char buffer[1024];

         if ((sockfd = socket(PF_INET, SOCK_STREAM,0)) == -1)

             fatal("in socket");

         if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR,&yes, sizeof(int)) == -1)

             fatal("setting socket optionSO_REUSEADDR");

      至此,程序已经利用socket()函数建立了一个套接字。我们想要一个TCP/IP套接字,因此协议族是用于IPv4PF_INET,套接字类型是用于流套接字的SOCK_STREAM。最后的协议参数是0,因为PF_INET协议族中只有一个协议。这个函数返回一个存储在sockfd中的套接字文件描述符。

      setsockopt()函数只是用于设置套接字选项。这个函数调用将SO_REUSEADDR套接字选项设置为true,这将允许它再次使用一个给定的地址进行绑定。如果不设置这个选项,程序试图绑定一个给定的端口时,若端口已在使用中,则会导致失败。如果没有正确关闭套接字,它可能看起来像是在使用中,因此这个选项让套接字绑定一个端口(并对其进行控制),即使端口是在使用中。

      这个函数的第1个参数是套接字(通过文件描述符引用),第2个参数指定选项的级别,第3个参数指定选项本身。因为SO_REUSEADDR是一个套接字级别选项,所以将级别设置为SOL_SOCKET。在/usr/include/asm/socket.h中定义了许多不同的套接字选项。最后的两个参数中,一个是指向数据的指针,选项值将被设置成这个数据,另一个是数据的长度。指向数据的指针和数据的长度是两个经常用于套接字函数的参数。它允许函数处理所有类型的数据,从单字节到大数据结构。SO_REUSEADDR选项的值是一个32位整数,因此要想将这个选项设true,最后两个参数必须指晌整型值1的指针和整型值的大小。(大小为4字节)。

      simple_server.c

         host_addr.sin_family = AF_INET;    // host byte order

         host_addr.sin_port = htons(PORT);  // short, network byte order

         host_addr.sin_addr.s_addr = INADDR_ANY; //automatically fill with my IP

         memset(&(host_addr.sin_zero), '\0', 8);// zero the rest of the struct

         if (bind(sockfd, (struct sockaddr*)&host_addr, sizeof(struct sockaddr)) == -1)

             fatal("binding to socket");

         if (listen(sockfd, 5) == -1)

            fatal("listeningon socket");

      接下来的几行建立了在绑定调用中使用的host_addr结构。地址族是AF_INET因为我们正在使用Ipv4sockaddr_in结构。将端口值设置为PORT,其值定义为7890。必须将这个短整型值转换成网络字节顺序,因此使用了htons()函数。地址设置为0,遂意味着会自动使用主机的当前IP地址填充地址。因为值0与字节顺序无关,所以这里不需要转换。

      bind()调用传递套接字文件描述符、地址结构和地址结构的长度。这个调用会将当前IP地址的套接字绑定到端口7890。

      listen()调用告诉套接字侦听传入连接,随后的accept()调用实际上接受了一个传入连接。listen()函数将所有的传入连接放入一个后备队列中,直到一个accept()接受连接为止。listen()的最后一个参数设置后备队列的最大尺寸。

      simple_server.c

      while(1){   // Accept loop

             sin_size = sizeof(struct sockaddr_in);

             new_sockfd = accept(sockfd, (structsockaddr *)&client_addr, &sin_size);

             if(new_sockfd == -1)

                 fatal("acceptingconnection");

             printf("server: got connection from%s port %d\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));

             send(new_sockfd, "HelloWorld!\n", 13, 0);

             recv_length = recv(new_sockfd,&buffer, 1024, 0);

             while(recv_length > 0) {

                 printf("RECV: %d bytes\n",recv_length);

                 dump(buffer, recv_length);

                 recv_length = recv(new_sockfd,&buffer, 1024, 0);

             }

             close(new_sockfd);

         }

         return 0;

      }

      接下来是一个接受传入连接的循环。我们马上就应该知道accept()函数前两个参数的意义而最后一个参数是指向地址结构大小的指针。这是因为accept()函数会将连接的客户信息写入地址结构,并将结构的大小写入sin_size。考虑到我们的目的结构的大小从不会变化,但要想使用这个函数,我们必须遵守调用约定。Accept()函数为已接受的连接返回一个新套接字文件描述符。以这种方式,可以继续使用原来的套接字文件描述符来接受新连接,而新文件描述符用于与已连接的客户端通信。

      获得一个连接后,程序会打印出一个连接信息,使用inet_ntoa()将sinaddr地址结构转换成dotted-number IP字符串,使用ntohs()来转换sin_port编号的字节顺序。

      Send()函数向表示新连接的新套接字发送13个字节的字符串“Hello,worldr\n”。send()和recv()函数的最后一个参数是标记,对我们的程序来说,它一直是0。

      接下来是一个从连接接收数据并打印的循环。程序为recv()函数提供了一个指向缓禄存区的指针和一个从套接字读取数据的最大长度。函数将数据写入传递给它的缓存区,并返回它实际上写入的字节个数。只要recv()调用持续接收数据的话,这个循环就会继续。

      编译并运行时,程序绑定主机的端口7890并等待传入的连接:


    • Telnet客户端的工作原理本质上来讲类似于一般的TCP连接客户端,所以可以使用它通过指定目标IP地址和端口来连接简单服务器。

      从远程机器上连接的代码示意



    • 上面的连接中,服务器发送字符串“Hello,world!”,其余部分是键入的“thisis a test file”和“;laskdjflasjdflkalskdjflasfd”在键盘上胡乱打出的一行字符的回显。因为Telnet采用行缓冲方式,所以按下Enter键时,这两行中的每一行都会被发送到服务器。回到服务器端,其输出显示了连接信息以及传送过来的数据包。

      在本地机器上连接的代码示意


      六、 一个Web客户端示例


      对于我们的服务器来说,Telnet程序是一个工作良好的客户端,因此实际上没有理由专门编写一个客户端。但是,有数千种接受标准TCP/IP连接的不同类型的服务器。每次使用一个Web浏览器时,它就会与某处的一台Web服务器产生一个连接。这个连接使用HTTP来传递网页,HTTP定义了一种请求和发送信息的方法。默认情况下,Web服务器在端口80上运行,这个端口与许多其他端口一起在/etc/services中列出。


      来自文件/etc/services


      finger         79/tcp


      finger         79/udp


      http           80/tcp         www www-http    # WorldWideWeb HTTP


      http           80/udp         www www-http   # HyperText Transfer Protocol


    • HTTP存在于应用层,即OSI模型的最顶层。在这一层上,较低层已经负责了所有的网络细节,因此HTTP的结构使用纯文本形式。许多其他的应用层协议也使用纯文本,例如POP3SMTP. IMAPFTP的控制通道。因为这些都是标准协议,所以它们的文档资料齐全且易于研究。一旦知道了这些不同协议的语法,您就可以手动与其他讲相同语言的程序进行交流了。虽然并不需要有多流利,但是向外部服务器行进时,了解一些重要的习惯用语会对您有所帮助。在HTTP语言中,使用命令GET来产生请求,后面紧跟资源路径和HTTP协议版本。例如,“GET/HTTP/1.O”会使用HTTP版本1.0请求Web服务器的根目录文档。实际上是请求根目录/,但大多数Web服务器会自动在该目录上搜索默认的HTML文档index.html。如果服务器找到了这个资源,它会使用HTTP,在发送内容之前通过发送若干个报头来作出响应。如果使用命令HEAD而不是GET,它将只返回HTTP报头而不返回内容。这些报头是纯文本的,并且通常可以提供服务器的相关信息。可以利用Telnet连接到一个已知网址,然后键入“HEAD/HTTP/1.O”并按Enter键两次来手工获取这些报头。在下面的输出中,Telnet用来打开一个与http://www.internic.net之间的TCP/IP连接,然后HTTP应用层手动请求主索引页的报头。

      UNIXshell下执行telnet命令时,连接到远端机后使用“escape”字符可进入telnet命令模式,此模式下用户可以输入telnet能够解释的命令,来控制telnet或设定与telnet相关的参数。默认的“escape”字符为“Ctrl+]”。用户可以使用set命令修改“escape”字符的默认值。



    • 下面的几个程序会发送和接收许多数据。因为标准套接字函数使用起来并不方便,所以我们编写了一些用于发送和接收数据的函数。这些函数(名称为send_string()recv_line()),会被添加到一个新的叫作hacking-network.h的包含文件中。


      普通的send()函数返回写入的字节个数,这个数并不总是等于您试图发送的字节个数。Send_string()函数接受一个套接字和一个字符串指针作为参数,并确认整个字符串已经通通过套接字发送。它使用strlen()计算传递给它的字符串的总长度。

      您也许已经注意到简单服务器收到的每个数据包都是以字节0x0D0x0A作为结尾。这正是Telnet结束行的方式——即发送一个回车符和一个换行符。HTTP协议也期望行以这两个字节结束。快速查看一下ASCII表可以发现0x0D是一个回车符(\r)0x0A是一个换行行符(\n)


    • 函数recv_line()读取整行数据。它从作为第1个参数传递的套接字中将数据读入第2个参数指向的缓冲区。在遇到按顺序排列的最后两个行结束符之前,它会持续从套接字接收数据,然后结束字符串并退出函数。这些新函数保证了作为行发送和接收的所有字节都以“\r\n”结尾。它们在下面一个称为hacking-network.h的新包含又件中列出。


      hacking-network.h


      /*这个函数接收一个套接字FD和一个指向nullptr,用来终止字符的发送。该函数会确保字符串的所有内容都已发送。返回1表示发送成功,返回0表示发送失败*/


      intsend_string(int sockfd, unsigned char *buffer) {


        int sent_bytes, bytes_to_send;


        bytes_to_send = strlen(buffer);


        while(bytes_to_send > 0) {


           sent_bytes = send(sockfd, buffer,bytes_to_send, 0);


           if(sent_bytes == -1)


              return 0; // return 0 on send error


           bytes_to_send -= sent_bytes;


           buffer += sent_bytes;


        }


        return 1; // return 1 on success


      }


       


      /*这个函数接收一个套接字FD和一个指向目的缓冲区的ptr。函数会一直从套接字接收数据,直到看到BOL字节是从套接字读取的,但是目的缓冲区在这些字节到达前就终止了,返回的是所读取行的大小(不包括BOL字节)*/


      intrecv_line(int sockfd, unsigned char *dest_buffer) {


      #define EOL"\r\n" // End-Of-Line byte sequence


      #defineEOL_SIZE 2


        unsigned char *ptr;


        int eol_matched = 0;


       


        ptr = dest_buffer;


        while(recv(sockfd, ptr, 1, 0) == 1) { //read a single byte


           if(*ptr == EOL[eol_matched]) { // doesthis byte match terminator


              eol_matched++;


              if(eol_matched == EOL_SIZE) { // ifall bytes match terminator,


                 *(ptr+1-EOL_SIZE) = '\0'; //terminate the string


                 return strlen(dest_buffer); //return bytes recevied


              }


           } else {


              eol_matched = 0;


           }  


           ptr++; // increment the pointer to thenext byter;


        }


        return 0; // didn't find the end of linecharacters


      }


      个套接字与一个数字IP地址连接很简单,但是为了方便,普遍使用的是命名地址。在手动HTTP HEAD请求中,Telnet程序自动执行DNSDomain Name Service,域名服务)查找来将www.internic.net转换为IP地址192.0.34.161DNS是一种允许按名称地址对IP地址进行查询的协议,类似于知道姓名时在电话簿中查找电话号码的过程。因此,自然存在有特别用于借助DNS查找主机名称的与套接字相关的函数和结构。这些函数和结构在netdb.h中定义。一个名为gethostbyname()的函数接受一个指向包含着命名地址的字符串作为参数,并返回一个指向hostent结构的指针,错误时返回NULL指针。Hostent结构中填充的是从查找中获得的信息,包括以网络字节顺序表示的作为数字IP地址的32位整数。与inet_ntoa()函数类似,在函数中这个结构使用的内存是以静态方式分配的。这个结构正如在netdb.h中列出来的那样。


      来自文件/usr/include/netdb.h


      /* Descriptionof data base entry for a single host. */


      struct hostent


      {


       char *h_name;        /*Official name of host. */


       char **h_aliases;    /* Alias list. */


       int h_addrtype;      /* Host address type. */


       int h_length;        /*Length of address. */


       char **h_addr_list;      /* List of addresses from name server. */


      #defineh_addr h_addr_list[0]  /* Address, for backward compatibility. */


      };


      下面的代码演示了gethostbyname()函数的用法。


      来自文件host_lookup.c


      #include<stdio.h>


      #include<stdlib.h>


      #include<string.h>


      #include<sys/socket.h>


      #include<netinet/in.h>


      #include<arpa/inet.h>


      #include<netdb.h>


      #include"hacking.h"


      int main(intargc, char *argv[]) {


         struct hostent *host_info;


         struct in_addr *address;


         


         if(argc < 2) {


             printf("Usage: %s<hostname>\n", argv[0]);


             exit(1);


         }


         host_info = gethostbyname(argv[1]);


         if(host_info == NULL) {


             printf("Couldn't lookup %s\n",argv[1]);


         } else {


             address = (struct in_addr *) (host_info->h_addr);


             printf("%s has address %s\n",argv[1], inet_ntoa(*address));


         }


      }

      这个程序接受一个主机名称作为它的唯一参数并打印出IP地址。函数gethostbyname()返回一个指向hostent结构的指针,这个结构的元素h_addr中包含IP地址。一个指向该元素的指针被强制转换成in_addr指针,在随后调用inet_ntoa()时对该指针解除引用,因为inet_ntoa()期望一个in_addr结构作为其参数。下面显示了示例程序的输出。



    • 此基础上使用套接字函数创建一个Web服务器识别程序并不困难。




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值