socket编程基础4(实例分析)

本文介绍了在 Linux 环境下的 socket 编程常用函数用法及 socket 编程的一般规则和客户 / 服务器模型的编程应注意的事项和常遇问题的解决方法,并举了具体代 码实例。要理解本文所谈的技术问题需要读者具有一定 C 语言的编程经验和 TCP/IP 方面的基本知识。要实习本文的示例,需要 Linux 下的 gcc 编译平台支持。
   Socket 定义
  网络的 Socket 数据传输是一种特殊的 I/O Socket 也是一种文件描述符。 Socket 也具有一个类似于打开文件的函数调用 —Socket() ,该函数返回一个整型的 Socket 描述符,随后的连接建立、数据传输等操作都是通过该 Socket 实现的。常用 Socket 类型有两种:流式 Socket—SOCK_STREAM 和数据报式 Socket—SOCK_DGRAM 。流式是一种面向连接的 Socket ,针对于面向连接的 TCP 服务应用;数据报式 Socket 是一种无连接的 Socket ,对应于无连接的 UDP 服务应用。
  Socket 编程相关数据类型定义
 计算机数据存储有两种字节优先顺序:高位字节优先和低位字节优先。 Intenet 上数据以高位字节优先顺序在网络上传输,所以对于在内部是以低位字节优先方式存储数据的机器,在 Internet 上传输数据时就需要进行转换。
  我们要讨论的第一个结构类型是: struct sockaddr ,该类型是用来保存 socket 信息的:
   struct sockaddr {
   unsigned short sa_family; 
   char sa_data[14]; };
   sa_family 一般为 AF_INET sa_data 则包含该 socket IP 地址和端口号。
  另外还有一种结构类型:
   struct sockaddr_in {
   short int sin_family; 
   unsigned short int sin_port; 
   struct in_addr sin_addr; 
   unsigned char sin_zero[8]; 
   };
  这个结构使用更为方便。 sin_zero( 它用来将 sockaddr_in 结构填充到与 struct sockaddr 同样的长度 ) 应该用 bzero() memset() 函数将其置为零。指向 sockaddr_in 的指针和指向 sockaddr 的指针可以相互转换,这意味着如果一个函数所需参数类型是 sockaddr 时,你可以在函数调用的时候将一个指向 sockaddr_in 的指针转换为指向 sockaddr 的指针;或者相反。 sin_family 通常被赋 AF_INET in_port sin_addr 应该转换成为网络字节优先顺序;而 sin_addr 则不需要转换。
 我们下面讨论几个字节顺序转换函数:
  htons()--"Host to Network Short" ; htonl()--"Host to Network long"
  ntohs()--"Network to Host Short" ; ntohl()--"Network to Host Long"
 在这里, h 表示 "host" n 表示 "network" s 表示 "short" l 表示 "long"

打开 socket 描述符、建立绑定并建立连接
socket
函数原型为:
  int socket(int domain, int type, int protocol);
domain
参数指定 socket 的类型: SOCK_STREAM SOCK_DGRAM protocol 通常赋值 “0” Socket()调用返回一个整型socket描述符 ,你可以在后面的调用使用它。一旦通过 socket调用返回一个socket描述符,你应该将该socket与你本机上的一个端口相关联 (往往当你在设计服务器端程序时需要调用该函数。随后你就可以在该端口监听服务请求 ; 而客户端一般无须调用该函数)。 Bind 函数原型为
  int bind(int sockfd,struct sockaddr *my_addr, int addrlen);
  Sockfd 是一个 socket 描述符, my_addr 是一个指向包含有本机 IP 地址及端口号等信息的 sockaddr 类型的指针 ; addrlen 常被设置为 sizeof(struct sockaddr)
 最后,对于 bind 函数要说明的一点是,你可以用下面的赋值实现自动获得本机 IP 地址和随机获取一个没有被占用的端口号:
   my_addr.sin_port = 0; 
   my_addr.sin_addr.s_addr = INADDR_ANY; 
通过将 my_addr.sin_port 置为 0 ,函数会自动为你选择一个未占用的端口来使用。同样,通过将 my_addr.sin_addr.s_addr 置为 INADDR_ANY ,系统会自动填入本机 IP 地址。 Bind() 函数在成功被调用时返回 0 ;遇到错误时返回 “-1” 并将 errno 置为相应的错误号。另外要注意的是,当调用函数时,一般不要将端口号置为小于 1024 的值,因为 1~1024 是保留端口号,你可以使用大于 1024 中任何一个没有被占用的端口号。
   Connect() 函数用来与远端服务器建立一个 TCP 连接,其函数原型为:
   int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
  Sockfd 是目的服务器的 sockt 描述符; serv_addr 是包含目的机 IP 地址和端口号的指针。遇到错误时返回 -1 ,并且 errno 中包含相应的错误码。进行客户端程序设计无须调用 bind() ,因为这种情况下只需知道目的机器的 IP 地址,而客户通过哪个端口与服务器建立连接并不需要关心,内核会自动选择一个未被占用的端口供客户端来使用。
   Listen()—— 监听是否有服务请求
  在服务器端程序中,当 socket 与某一端口捆绑以后,就需要监听该端口,以便对到达的服务请求加以处理。
int listen(int sockfd
int backlog);
  Sockfd Socket 系统调用返回的 socket 描述符; backlog 指定在请求队列中允许的最大请求数,进入的连接请求将在队列中等待 accept() 它们 (参考下文)。 cklog 对队列中等待服务的请求的数目进行了限制,大多数系统缺省值为 20
listen 遇到错误时返回 -1 errno 被置为相应的错误码。
 故服务器端程序通常按下列顺序进行函数调用:
  socket(); bind(); listen(); 
accept()—— 连接端口的服务请求。
当某个客户端试图与服务器监听的端口连接时,该连接请求将排队等待服务器 accept() 它。通过调用 accept() 函数为其建立一个连接, accept() 函数将返回一个新的 socket 描述符,来供这个新连接来使用。而服务器可以继续在以前的那个 socket 上监听,同时可以在新的 socket 描述符上进行数据 send() (发送)和 recv() (接收)操作:
   int accept(int sockfd, void *addr, int *addrlen);
   sockfd 是被监听的 socket 描述符, addr 通常是一个指向 sockaddr_in 变量的指针,该变量用来存放提出连接请求服务的主机的信息(某台主机从某个端口发出该请求); addrten 通常为一个指向值为 sizeof(struct sockaddr_in) 的整型指针变量。错误发生时返回一个 -1 并且设置相应的 errno 值。
   Send() recv()—— 数据传输
  这两个函数是用于面向连接的 socket 上进行数据传输。
   Send() 函数原型为:
   int send(int sockfd, const void *msg, int len, int flags);
   Sockfd 是你想用来传输数据的 socket 描述符, msg 是一个指向要发送数据的指针。
   Len 是以字节为单位的数据的长度。 flags 一般情况下置为 0 (关于该参数的用法可参照 man 手册)。
   char *msg = "Beej was here!"; int len bytes_sent; ... ...
   len = strlen(msg); bytes_sent = send(sockfd, msg,len,0); ... ...
   Send() 函数返回实际上发送出的字节数,可能会少于你希望发送的数据。所以需要对 send() 的返回值进行测量。当 send() 返回值与 len 不匹配时,应该对这种情况进行处理。
   recv() 函数原型为:
   int recv(int sockfd,void *buf,int len,unsigned int flags);
   Sockfd 是接受数据的 socket 描述符; buf 是存放接收数据的缓冲区; len 是缓冲的长度 Flags 也被置为 0 Recv() 返回实际上接收的字节数 ,或当出现错误时,返回 -1 并置相应的 errno 值。
   Sendto() recvfrom()—— 利用数据报方式进行数据传输
  在无连接的数据报 socket 方式下,由于本地 socket 并没有与远端机器建立连接,所以在发送数据时应指明目的地址 sendto() 函数原型为:
   int sendto(int sockfd, const void *msg,int len,unsigned int flags, const struct sockaddr *to, int tolen);
  该函数比 send() 函数多了两个参数, to 表示目地机的 IP 地址和端口号信息 ,而 tolen 常常被赋值为 sizeof (struct sockaddr) Sendto 函数也返回实际发送的数据字节长度或在出现发送错误时返回 -1
   Recvfrom() 函数原型为:
   int recvfrom(int sockfd,void *buf,int len,unsigned int lags,struct sockaddr *from,int *fromlen);
   from 是一个 struct sockaddr 类型的变量,该变量保存源机的 IP 地址及端口号。 fromlen 常置为 sizeof (struct sockaddr) 。当 recvfrom() 返回时, fromlen 包含实际存入 from 中的数据字节数。 Recvfrom() 函数返回接收到的字节数或当出现错误时返回 -1 ,并置相应的 errno
  应注意的一点是,当你对于数据报 socket 调用了 connect() 函数时,你也可以利用 send() recv() 进行数据传输,但该 socket 仍然是数据报 socket ,并且利用传输层的 UDP 服务。但在发送或接收数据报时,内核会自动为之加上目地和源地址信息。
  Close() shutdown()—— 结束数据传输
 当所有的数据操作结束以后,你可以调用 close() 函数来释放该 socket ,从而
停止在该 socket 上的任何数据操作: close(sockfd);
你也可以调用 shutdown() 函数来关闭该 socket 。该函数允许你只停止在某个方向上的数据传输,而一个方向上的数据传输继续进行。 如你可以关闭某 socket 的写操作而允许继续在该 socket 上接受数据,直至读入所有数据。
   int shutdown(int sockfd,int how);
   Sockfd 的含义是显而易见的,而参数 how 可以设为下列值:
   ·0------- 不允许继续接收数据
   ·1------- 不允许继续发送数据
   ·2------- 不允许继续发送和接收数据,均为允许则调用 close ()
   shutdown 在操作成功时返回 0 ,在出现错误时返回 -1 (并置相应 errno )。

   DNS—— 域名服务相关函数
  由于 IP 地址难以记忆和读写,所以为了读写记忆方便,人们常常用域名来表示主机,这就需要进行域名和 IP 地址的转换。函数 gethostbyname() 就是完成这种转换的 ,函数原型为:
   struct hostent *gethostbyname(const char *name);
  函数返回一种名为 hosten 的结构类型,它的定义如下:
   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] 
  当 gethostname() 调用成功时,返回指向 struct hosten 的指针,当调用失败时返回 -1 当调用 gethostbyname 时,你不能使用 perror() 函数来输出错误信息,而应该使用 herror() 函数来输出。
  面向连接的客户 / 服务器代码实例
  这个服务器通过一个连接向客户发送字符串 "Hello world!" 。只要在服务器上运行该服务器软件,在客户端运行客户软件,客户端就会收到该字符串。
  该服务器软件代码见程序 1
  # i nclude stdio.h
  # i nclude stdlib.h
  # i nclude errno.h
  # i nclude string.h
  # i nclude sys/types.h
  # i nclude netinet/in.h
  # i nclude sys/socket.h
  # i nclude sys/wait.h
   #define MYPORT 3490 
   #define BACKLOG 10 
   main()
   {
   intsock fd,new_fd; 
   struct sockaddr_in my_addr; 
   struct sockaddr_in their_addr; 
   int sin_size;
   if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { 
   perror("socket"); exit(1); }
   my_addr.sin_family=AF_INET;
   my_addr.sin_port=htons(MYPORT);
  
my_addr.sin_addr.s_addr = INADDR_ANY;
   bzero(&(my_addr.sin_zero),8);
   if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) 
   == -1) {
   perror("bind"); exit(1); }
   if (listen(sockfd, BACKLOG) == -1) {
   perror("listen"); exit(1); }
   while(1) { 
   sin_size = sizeof(struct sockaddr_in);
   if ((new_fd = accept(sockfd, (struct sockaddr *)&their_addr, 
   &sin_size)) == -1) {
   perror("accept"); continue; }
   printf("server: got connection from %s",inet_ntoa(their_addr.sin_addr));
   if (!fork()) { 
   if (send(new_fd, "Hello, world!", 14, 0) == -1)
   perror("send"); close(new_fd); exit(0); }
   close(new_fd); 
   waitpid(-1,NULL,WNOHANG) > 0 
   }
   }
  (程序 1
  服务器首先创建一个 Socket ,然后将该 Socket 与本地地址 / 端口号捆绑,成功之后就在相应的 socket 上监听, accpet 捕捉到一个连接服务请求时,就生成一个新的 socket ,并通过这个新的 socket 向客户端发送字符串 "Hello world!" ,然后关闭该 socket
   fork() 函数生成一个子进程来处理数据传输部分, fork() 语句对于子进程返回的值为 0 。所以包含 fork 函数的 if 语句是子进程代码部分,它与 if 语句后面的父进程代码部分是并发执行的。
  客户端软件代码部分见程序 2
  # i ncludestdio.h
  # i nclude stdlib.h
  # i nclude errno.h
  # i nclude string.h
  # i nclude netdb.h
  # i nclude sys/types.h
  # i nclude netinet/in.h
  # i nclude sys/socket.h
   #define PORT 3490
   #define MAXDATASIZE 100 
   int main(int argc, char *argv[])
   {
   int sockfd, numbytes;
   char buf[MAXDATASIZE];
   struct hostent *he;
   struct sockaddr_in their_addr;
   if (argc != 2) {
   fprintf(stderr,"usage: client hostname"); exit(1); }
   if((he=gethostbyname(argv[1]))==NULL) {
   herror("gethostbyname"); exit(1); }
   if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
   perror("socket"); exit(1); }
   their_addr.sin_family=AF_INET;
   their_addr.sin_port=htons(PORT);
   their_addr.sin_addr = *((struct in_addr *)he->h_addr);
   bzero(&(their_addr.sin_zero),8);
   if (connect(sockfd, (struct sockaddr *)&their_addr, 
   sizeof(struct sockaddr)) == -1) {
   perror("connect"); exit(1); }
   if ((numbytes=recv(sockfd, buf, MAXDATASIZE, 0)) == -1) {
   perror("recv"); exit(1); }
   buf[numbytes] = '';
   printf("Received: %s",buf);
   close(sockfd);
   return 0;
   }
  (程序 2
 客户端代码相对来说要简单一些,首先通过服务器域名获得其 IP 地址,然后创建一个 socket ,调用 connect 函数与服务器建立连接,连接成功之后接收从服务器发送过来的数据,最后关闭 socket ,结束程序。
 无连接的客户 / 服务器程序的在原理上和连接的客户 / 服务器是一样的,两者的区别在于无连接的客户 / 服务器中的客户一般不需要建立连接,而且在发送接收数据时,需要指定远端机的地址。
  关于阻塞 (blocking) 的概念和 select() 函数当服务器运行到 accept 语句时,而没有客户连接服务请求到来,那么会发生什么情况 ? 这时服务器就会停止在 accept 语句上等待连接服务请求的到来;同样,当程序运行到接收数据语句时,如果没有数据可以读取,则程序同样会停止在接收语句上。这种情况称为 blocking 。但如果你希望服务器仅仅注意检查是否有客户在等待连接,有就接受连接 ; 否则就继续做其他事情,则可以通过将 Socke 设置为非阻塞方式来实现 : 非阻塞 socket 在没有客户在等待时就使 accept 调用立即返回。
  # i nclude unistd.h
  # i nclude fcntl.h
   . . . . sockfd = socket(AF_INET,SOCK_STREAM,0);
   fcntl(sockfd,F_SETFL,O_NONBLOCK) . . . . .
  通过设置 socket 为非阻塞方式,可以实现 轮询 若干 Socket 。当企图从一个没有数据等待处理的非阻塞 Socket 读入数据时,函数将立即返回,并且返回值置为 -1 ,并且 errno 置为 EWOULDBLOCK 但是这种 轮询 会使 CPU 处于忙等待方式,从而降低性能。 考虑到这种情况,假设你希望服务器监听连接服务请求的同时从已经建立的连接读取数据,你也许会想到用一个 accept 语句和多个 recv() 语句,但是由于 accept recv 都是会阻塞的 ,所以这个想法显然不会成功。
  调用非阻塞的 socket 会大大地浪费系统资源。而调用 select() 会有效地解决这个问题,它允许你把进程本身挂起来,而同时使系统内核监听所要求的一组文件描述符的任何活动,只要确认在任何被监控的文件描述符上出现活动, select() 调用将返回指示该文件描述符已准备好的信息,从而实现了为进程选出随机的变化 ,而不必由进程本身对输入进行测试而浪费 CPU 开销。 Select 函数原型为 :
  int select(int numfds,fd_set *readfds,fd_set *writefds fd_set *exeptfds,struct timeval *timeout);
其中 readfds writefds exceptfds 分别是被 select() 监视的读、写和异常处理的文件描述符集合 。如果你希望确定是否可以从标准输入和某个 socket 描述符读取数据,你只需要将标准输入的文件描述符 0 和相应的 sockdtfd 加入到 readfds 集合中; numfds 的值是需要检查的号码最高的文件描述符加 1 ,这个例子中 numfds 的值应为 sockfd+1 ;当 select 返回时, readfds 将被修改,指示某个文件描述符已经准备被读取,你可以通过 FD_ISSSET() 来测试。为了实现 fd_set 中对应的文件描述符的设置、复位和测试,它提供了一组宏:
   FD_ZERO(fd_set *set)---- 清除一个文件描述符集;
   FD_SET(int fd,fd_set *set)---- 将一个文件描述符加入文件描述符集中;

   FD_CLR(int fd,fd_set *set)---- 将一个文件描述符从文件描述符集中清除

   FD_ISSET(int fd,fd_set *set)---- 试判断是否文件描述符被置位。
   Timeout 参数是一个指向 struct timeval 类型的指针,它可以使 select() 在等待 timeout 长时间后没有文件描述符准备好即返回。 struct timeval 数据结构为:

   struct timeval {
   int tv_sec; 
   int tv_usec; 
   };
  我们通过程序 3 来说明:
  # i nclude sys/time.h
  # i nclude sys/types.h
  # i nclude unistd.h
   #define STDIN 0 
   main()
   {
   struct timeval tv;
   fd_set readfds;
   tv.tv_sec = 2;
   tv.tv_usec = 500000;
   FD_ZERO(&readfds);
   FD_SET(STDIN,&readfds);
  
   select(STDIN+1 &readfds NULL NULL &tv);
   if (FD_ISSET(STDIN &readfds)) printf("A key was pressed!");

   else printf("Timed out.");
   }
  (程序 3
   select() 在被监视端口等待 2.5 秒钟以后,就从 select 返回

你可能感兴趣的:(socket编程基础4(实例分析))