Chinaunix首页 | 论坛 | 博客
  • 博客访问: 134548
  • 博文数量: 19
  • 博客积分: 251
  • 博客等级: 入伍新兵
  • 技术积分: 136
  • 用 户 组: 普通用户
  • 注册时间: 2011-06-15 14:15
文章分类

全部博文(19)

文章存档

2016年(11)

2011年(8)

我的朋友

分类: C/C++

2011-09-07 18:04:36

 

     本文先对socket进行简单的介绍,然后详细讲解socket编程的步骤及每一步作用,最后通过一个实例实现客户端与服务器端的通信,代码经测试可用。

 

1 、什么是 Socket

    Socket 接口是TCP/IP 网络的 APISocket 接口定义了许多函数或例程,程序员可以用它们来开发 TCP/IP 网络上的应用程序。要学 Internet 上的 TCP/IP 网络编程,必须理解 Socket 接口。

 Socket 接口设计者最先是将接口放在 Unix 操作系统里面的。如果了解 Unix 系统的输入和输出的话,就很容易了解 Socket 了。网络的 Socket 数据传输是一种特殊的 I/O Socket 也是一种文件描述符 Socket 也具有一个类似于打开文件的函数调用 Socket() ,该函数返回一个整型的 Socket 描述符,随后的连接建立、数据传输等操作都是通过该 Socket 实现的。常用的 Socket 类型有两种:

 流式 Socket SOCK_STREAM )和数据报式 Socket SOCK_DGRAM )。

 流式是一种面向连接的 Socket ,针对于面向连接的 TCP 服务应用;

 数据报式 Socket 是一种无连接的 Socket ,对应于无连接的 UDP 服务应用。

 

2 Socket 建立

 为了建立 Socket ,程序可以调用 Socket 函数,该函数返回一个类似于文件描述符的句柄。

 socket 函数原型为: int socket(int domain, int type, int protocol);

 domain 指明所使用的协议族,通常为 AF_INET ,表示互联网协议族( TCP/IP 协议族); type 参数指定 socket 的类型: SOCK_STREAM SOCK_DGRAM Socket 接口还定义了原始 Socket SOCK_RAW ),允许程序使用低层协议;

 protocol 通常赋值 "0"

 Socket() 调用返回一个整型 socket 描述符,你可以在后面的调用使用它

  Socket 描述符是一个指向内部数据结构的指针 ,它指向描述符表入口。调用 Socket函数时,socket执行体将建立一个 Socket ,实际上 " 建立一个 Socket" 意味着为一个Socket数据结构分配存储空间。Socket执行体为你管理描述符表。

  两个网络程序之间的一个网络连接包括五种信息:通信协议 本地协议地址 本地主机端口 远端主机地址 远端协议端口 Socket 数据结构中包含这五种信息。

 

3 Socket 配置

 通过 socket 调用返回一个 socket 描述符后,在使用 socket 进行网络传输以前,必须配置该 socket.

 面向连接socket 客户端 通过调用 Connect 函数在 socket 数据结构中保存本地和远端信息。(ps:这时候客户端这边不需要bind,服务器端需要bind)

 无连接socket客户端和服务端以及面向连接 socket 服务端通过调用 bind 函数来配置本地信息Bind 函数将 socket 与本机上的一个端口相关联,随后你就可以在该端口监听服务请求。

     Bind 函数原型:

     int bind(int sockfd,struct sockaddr *my_addr, int addrlen);

   Sockfd 是调用 socket 函数返回的 socket 描述符 ,my_addr 是一个指向包含有本机 IP 地址及端口号等信息的 sockaddr 类型的指针; addrlen 常被设置为 sizeof(struct sockaddr)

   struct sockaddr 结构类型是用来保存 socket 信息的:

   struct sockaddr {

                    unsigned short sa_family; /* 地址族, AF_xxx */

  char sa_data[14]; /* 14 字节的协议地址 */

    };

sa_family 一般为 AF_INET ,代表 Internet TCP/IP )地址族;sa_data 则包含该 socket IP 地址和端口号。

   另外还有一种结构类型

   struct sockaddr_in {

                    short int sin_family; /* 地址族 */

                    unsigned short int sin_port; /* 端口号 */

                    struct in_addr sin_addr; /* IP 地址 */

                    unsigned char sin_zero[8]; /* 填充 0 以保持与struct sockaddr 同样大小 */

   };

   这个结构更方便使用。 sin_zero 用来将 sockaddr_in 结构填充到与 struct sockaddr 同样的长度,可以用 bzero() memset() 函数将其置为零。指向 sockaddr_in 的指针和指向 sockaddr 的指针可以相互转换,这意味着如果一个函数所需参数类型是 sockaddr 时,你可以在函数调用的时候将一个指向 sockaddr_in 的指针转换为指向 sockaddr 的指针;或者相反。

   使用 bind 函数时,可以用下面的赋值实现自动获得本机 IP 地址和随机获取一个没有被占用的端口号:

   my_addr.sin_port = 0; /* 系统随机选择一个未被使用的端口号 */

   my_addr.sin_addr.s_addr = INADDR_ANY; /* 填入本机 IP 地址 */

通过将 my_addr.sin_port 置为0,函数会自动为你选择一个未占用的端口来使用。

同样,通过将 my_addr.sin_addr.s_addr 置为 INADDR_ANY ,系统会自动填入本机 IP 地址。注意在使用 bind 函数是需要将 sin_port sin_addr 转换成为网络字节优先顺序sin_addr 则不需要转换。

   计算机数据存储有两种字节优先顺序:高位字节优先和低位字节优先。 Internet 上数据以高位字节优先顺序在网络上传输 ,所以对于在内部是以低位字节优先方式存储数据的机器,在 Internet 上传输数据时就需要进行转换,否则就会出现数据不一致。

   下面是几个字节顺序转换函数:

·htonl() :把 32 位值从主机字节序转换成网络字节序

·htons() :把 16 位值从主机字节序转换成网络字节序

·ntohl() :把 32 位值从网络字节序转换成主机字节序

·ntohs() :把 16 位值从网络字节序转换成主机字节序

  Bind() 函数在成功被调用时返回 0 ;出现错误时返回 "-1" 并将 errno 置为相应的错误号。

 

需要注意的是,在调用 bind 函数时一般不要将端口号置为小于 1024 的值 ,因为 1 1024 是保留端口号 ,你可以选择大于 1024 中的任何一个没有被占用的端口号。

 

4 、连接建立

  面向连接的客户程序使用 Connect 函数来配置 socket 并与远端服务器建立一个 TCP 连接,其函数原型为:

  int connect(int sockfd, struct sockaddr *serv_addr,int addrlen);

Sockfd socket 函数返回的 socket 描述符;

serv_addr 是包含远端主机 IP 地址和端口号的指针;

addrlen 是远端地址结构的长度。

Connect 函数在出现错误时返回 -1 ,并且设置 errno 为相应的错误码。

进行客户端程序设计无须调用 bind() 因为这种情况下只需知道目的机器的 IP 地址 而客户通过哪个端口与服务器建立连接并不需要关心 socket 执行体为你的程序自动选择一个未被占用的端口,并通知你的程序数据什么时候到打断口。

  Connect 函数启动和远端主机的直接连接。只有面向连接的客户程序使用 socket 时才需要将此 socket 与远端主机相连 。无连接协议从不建立直接连接。面向连接的服务器也从不启动一个连接,它只是被动的在协议端口监听客户的请求。

  Listen 函数使 socket 处于被动的监听模式,并为该 socket 建立一个输入数据队列,将到达的服务请求保存在此队列中,直到程序处理它们。

  int listen(int sockfd int backlog);

Sockfd Socket 系统调用返回的 socket 描述符;

backlog 指定在请求队列中允许的最大请求数,进入的连接请求将在队列中等待 accept() 它们(参考下文)。 Backlog 对队列中等待服务的请求的数目进行了限制,大多数系统缺省值为 20 。如果一个服务请求到来时,输入队列已满,该 socket 将拒绝连接请求,客户将收到一个出错信息。当出现错误时 listen 函数返回 -1 ,并置相应的 errno 错误码。

  accept() 函数让服务器接收客户的连接请求 。在建立好输入队列后,服务器就调用 accept 函数,然后睡眠并等待客户的连接请求。

  int accept(int sockfd, void *addr, int *addrlen);

  sockfd 是被监听的 socket 描述符, addr 通常是一个指向 sockaddr_in 变量的指针,该变量用来存放提出连接请求服务的主机的信息(某台主机从某个端口发出该请求); addrlen 通常为一个指向值为 sizeof(struct sockaddr_in) 的整型指针变量。出现错误时 accept 函数返回 -1 并置相应的 errno 值。

  首先,当 accept 函数监视的 socket 收到连接请求时, socket 执行体将建立一个新的 socket ,执行体将这个新 socket 和请求连接进程的地址联系起来,收到服务请求的初始 socket 仍可以继续在以前的 socket 上监听,同时可以在新的 socket 描述符上进行数据传输操作。

 

5 、数据传输

    send() recv() 这两个函数用于面向连接socket 上进行数据传输。

  send() 函数原型为:

  int send(int sockfd, const void *msg, int len, int flags);

Sockfd 是你想用来传输数据的 socket 描述符;

msg 是一个指向要发送数据的指针;

Len 是以字节为单位的数据的长度;

flags 一般情况下置为 0 (关于该参数的用法可参照 man 手册)。

  Send() 函数返回实际上发送出的字节数,可能会少于你希望发送的数据。在程序中应该将 send() 的返回值与欲发送的字节数进行比较。当 send() 返回值与 len 不匹配时,应该对这种情况进行处理。

char *msg = "Hello!";

int len, bytes_sent;

……

len = strlen(msg);

bytes_sent = send(sockfd, msg,len,0);

……

   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 flags,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 服务 。但在发送或接收数据报时, 内核会自动为之加上目地和源地址信息

 

6 、结束传输

   当所有的数据操作结束以后,你可以调用 close() 函数来释放该 socket ,从而停止在该 socket 上的任何数据操作: close(sockfd);

  你也可以调用 shutdown() 函数来关闭该 socket 。该函数允许你只停止 在某个方向上的数据传输,而一个方向上的数据传输继续进行。如你可以关闭某 socket 的写操作而允许继续在该 socket 上接受数据,直至读入所有数据。

  int shutdown(int sockfd,int how);

  Sockfd 是需要关闭的 socket 的描述符。参数 how 允许为 shutdown 操作选择以下几种方式:

   ·0------- 不允许继续接收数据

   ·1------- 不允许继续发送数据

  ·2------- 不允许继续发送和接收数据,

  · 均为允许则调用 close ()

   shutdown 在操作成功时返回 0 ,在出现错误时返回 -1 并置相应 errno

 

7 、面向连接的 Socket 实例

  代码实例中的服务器通过 socket 连接客户端。

 该服务器软件代码如下:

#include

#include

#include

#include

#include //inet_aton...

#include

 

int net_server_init ( const struct sockaddr_in *addr, int addr_len);

 

int main ()

{

  int sockfd;

  int addr_len;

  struct sockaddr_in server_addr;

  int port;

 

  port = 5003;

  addr_len = sizeof ( struct sockaddr_in);

  server_addr. sin_family = AF_INET;

  server_addr. sin_addr . s_addr = INADDR_ANY; //本机地址

  server_addr. sin_port = htons (port);

 

  //connect to client

  sockfd = net_server_init(&server_addr, addr_len);

  if (!(sockfd<0)){

      printf "connect OK!/n" );

  }

  else {

     printf( "connect error!/n" );

  }

  close (sockfd);

  return 0;

}

 

int net_server_init ( const struct sockaddr_in *addr, int addr_len)

{

  int socket_fd; //socket descriptor

  int optval;

 

  socket_fd = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP);

  if (socket_fd == -1) {

      printf( "Failed to init socket/n" );

  return -1;

}

  /*--Reuse ip address and port.*/

  optval = 1;

  if ( setsockopt (socket_fd, SOL_SOCKET, SO_REUSEADDR, &optval,

       sizeof (optval)) < 0) {

   printf( "Failed to set address reuse./n" );

}

  if ( bind (socket_fd, ( struct sockaddr *)addr, addr_len) < 0) {

       printf( "Failed to bind socket: %s/n" );

  return -1;

}

  if ( listen (socket_fd, BACKLOG) < 0) {

       printf( "Failed to listen socket: %s/n" );

  return -1;

}

 

  return socket_fd;

}

  服务器的工作流程是这样的:首先调用 socket 函数创建一个 Socket ,然后调用 bind 函数将其与本机地址以及一个本地端口号绑定,然后调用 listen 在相应的 socket 上监听,当 accpet 接收到一个连接服务请求时,将生成一个新的 socket 。  

 

客户端程序代码如下:

#include

#include

#include //memcpy()..memset()..strlen()...

#include

#include //inet_aton...

#include //close()

 

int net_client_init ( const char *ip, int port);

 

int main ()

{

  int sockfd;

 

  //connect to server

  sockfd = net_client_init( "192.168.1.96" , 5003);

  if (!(sockfd<0)){

      printf( "connect OK!/n" );

  }

  else {

      printf( "connect error!/n" );

  }

  close(sockfd);

  return 0;

}

 

int net_ client_ init ( const char *ip, int port)

{

  int socket_fd;

  int addr_len;

  struct sockaddr_in *client_addr;

 

  client_addr=(struct sockaddr_in *) malloc(sizeof(struct sockaddr_in));

  memset (client_addr, 0, sizeof ( struct sockaddr_in));

  addr_len = sizeof ( struct sockaddr_in);

  client_addr-> sin_family =AF_INET;

 

  //convert string ip to 32 bit net serial ip

  if ( inet_aton (ip, &client_addr-> sin_addr ) == 0){

       printf("failed to convert ip address form./n" );

  return -1;

   }

   client_addr-> sin_port = htons (port); //2 byte host to net

 

   socket_fd = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP);

   if (socket_fd == -1) {

       printf("Failed to init socket/n" );

   return -1;

   }

 

   if ( connect (socket_fd, ( const struct sockaddr *)client_addr, addr_len) < 0){

       printf("Failed to connect/n" );

   return -1;

    }

 

   free (client_addr);

   client_addr = NULL;

 

   return socket_fd;

}

在客户端和服务器端分别用gcc编译,然后先运行服务器端可执行程序,这时候服务器端处于监听状态,然后在运行客户端可执行程序,连接服务器。连接成功则分别输出“connect ok!”。

阅读(2389) | 评论(0) | 转发(2) |
给主人留下些什么吧!~~