Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1811827
  • 博文数量: 272
  • 博客积分: 1272
  • 博客等级: 少尉
  • 技术积分: 1866
  • 用 户 组: 普通用户
  • 注册时间: 2011-03-09 15:51
文章分类

全部博文(272)

文章存档

2016年(16)

2015年(28)

2014年(97)

2013年(59)

2012年(25)

2011年(47)

分类:

2011-07-17 22:25:25

原文地址:Socket 编程实现 作者:erikyo123

一、             定义

在同一个设备上运行的进程之间的通信有很多种方式,包括管道、FIFO、消息队列、信号量以及共享内存。然而如果要实现不在同一台设备上运行的进程之间的通信就需要利用socket接口。

Socket通信既可以用于不在同一台设备上运行的几个进程之间的通信,也可以用于运行在同一台设备上的几个进程之间的通信。

本文主要介绍在因特网上通信的socket编程。

 

二、             基本概念

socket通信的学习过程中,我们需要先解释几个概念,以方便后续的学习。

 

       域(domain):

他确定通信的特性,包括地址格式。各个域都有自己的格式表示地址,而表示各个域的常数都以AF_开头,意指地址族(address family)。通常有以下几类socket通信域:

AF_INET              IPv4因特网域

AF_INET6            IPv6因特网域

AF_UNIX             UNIX

AF_UNSOEC        未指定

 

       套接字类型:

              套接字的类型进一步确定了通信的特征,主要有以下类型可以选择:

              SOCK_DGRAM                  长度固定的、无连接的不可靠报文传递

              SOCK_RAW                       IP协议的数据包接口

              SOCK_SEQPACKET           长度固定、有序、可靠的面向连接报文传递

              SOCK_STREAM                 有序、可靠、双向的面向连接字节流

             

       SOCK_DGRAMSOCK_STREAM这两种类型的是在应用中使用较频繁的两种套接字类型。前者是基于无连接的,即UDP传输,后续是基本面向连接的,即TCP传输。

 

字节序:

       在进行socket通信的时候,需要注意字节序的类型。通常的计算机中存在着两种不同的字节序:大端字节序和小端字节序。这两种字节序的主要区别在于最大字节地址和数字最低有效字节的关系。大端字节序是最大字节地址对应最低有效字节,反之既是小端字节序。因此当用一个指针cp指向保存着0x04030201的内存时,cp[0] = 4 cp[3] = 0,小端字节序则是cp[0] = 0, cp[3] = 4

       TCP/IP协议中所以的地址都是大端模式的,而linux系统的地址则通常是小端模式的。因此需要有相关的函数进行转化:

       uint32_t htonl(uint32_t hostint32)        32位主机地址装成网络地址格式

       uint16_t htonl(uint16_t hostint16)        16位主机地址装成网络地址格式

       uint32_t ntohl(uint32_t netint32)         32位网络地址装成主机地址格式

       uint16_t ntohl(uint16_t netint16)         16位网络地址装成主机地址格式

主机地址和端口号

       socket通信中,一个进程通常需要绑定自身的IP地址来告知想要与其进行通信的另一进程发送请求的目的地,以及一个端口号来告知接受请求的计算机中对应的进程。因此建立socket通信必须提供对应的主机地址和端口号。

       IPv4中主机地址是32位的,但是为了记忆方便我们通常将其表示为十进制字符串(192.168.1.1),linux提供了两个接口用于二进制表示和十进制字符串表示的转换。

       const char *inet_ntop(int domain, void *restrict addr, char *restrict str, socklen_t size)

       成功返回字符串表达式的地址指针,失败NULL

       int inet_pton(int domain, const char *restrict str, void *restrict addr),

成功返回1,格式无效返回0,若出错返回-1Addr中存放转换之后的网络地址

 

     系统中01024的端口号是为特定的服务预留的(ftp 21, http 80),如果想要使用则要有超级用户的权限。通常我们使用的端口号是从1025开始的。

 

       主机名和服务:

Socket通信的建立需要提供地址和端口号,但是我们也可以使用主机名和服务名来建立socket通信。通过调用getaddrinfo函数可以将主机名和服务名字映射到一个称为struct addrinfo的结构内部。

int getaddrinfo(const char *restrict host, const char *restrict service,

const char *restrict hint, struct addrinfo **restrict res)

              成功返回0,失败返回-1.

       该函数需要用户输入主机名host,服务名service,以及筛选信息hint,返回之后可以在res中得到可以用于bind以及connect接口的地址。(struct addrinfo 的具体信息感兴趣的同学请查阅unix手册)

 

三、             数据结构

在使用socket的接口函数时会遇到一些保存socket通信所需信息的数据结构。其中最重要的一个结构是sockaddr定义如下

struct sockaddr

{

        sa_family_t sa_family;  /*address family*/

        char         sa_data[ ];  /*variable-length address */

};

该结构是众多socket接口函数的主要参数类型,许多传入接口函数的变量,将被强制转换成该结构。在IPv4因特网域中(AF_INET),套接字用如下数据结构表示(linux中的定义)

struct sockaddr_in

{

        sa_family_t    sin_family;          /*address family */

        in_port_t      sin_port;              /*port number */

        struct in_addr  sin_addr;             /*IPv4 address */

unsigned char  sin_zero[8];         /*filler */

};

当用户将该数据结构类型的变量传给struct sockaddr类型的变量的接口函数的时候,都需要强制转换为struct sockaddr类型。

四、             socket编程步骤

需要socket通信的进程之间的关系,可以看成serverclient的关系。在一台设备上运行一个server进程,同时需要在另一台主机上运行一个或多个client进程。为了使client进程可以和server进程通信,我们需要进程一下步骤的编程:

 

客户端:

1)建立socket描述符。socket描述符与文件描述符类似,是后续socket接口操作的入口。事实上readwrite也可以操作socket描述符。

#include

int socket(int domain, int type, int protocol)

返回值:成功返回socket描述符,失败返回 -1

备注:domaintype既是前面说过的域和套接字类型。Protoclo是选择的协议,通常我们使用0来设置默认协议。AF_INETSOCK_DGRAM默认的协议是UDPSOCK_STREAM默认的协议是TCP

 

2)为struct sockaddr_in类型的变量成员赋值,该变量是后续建立连接调用connect函数的参数。主要包括地址族sa_family,端口号port以及地址sin_addr

 

3)调用connect接口与服务器进程建立连接。默认情况下Connet函数通常会阻塞直到服务器接受该连接后返回。

int connect(int sockfd, struct sockaddr *adr, socklen_t len)

成功返回0,失败返回-1

 

4)建立连接之后使用通信接口函数实现进程之间的数据通信。有四对接口用于发送与接收数据。

 

ssize_t read(int fd, void*buf, size_t count);

ssize_t write(int fd, void*buf, size_t count);

readwrite的使用和文件操作是一样的,只是第一个参数换成socket描述符。

 

ssize_t send(int sockfd, const void*buf, size_t nbytes, int flags)

ssize_t recv(int sockfd, const void* buf, size_t nbytes, int flags)

这两个函数的使用和read/write函数对类似,只是多了一个flags标志,该标志用来改变传输数据的方式。(有兴趣的同学查阅unix手册)

 

以上两个函数是用于面向连接的STREAM类型的套接字,还有与这两个函数很相似的sendto函数和recvfrom函数可以用于无连接的DGRAM类型套接字。

ssize_t sendto(int sockfd, const void*buf, size_t nbytes, rint flags,

const struct sockaddr *destaddr, socklen_t destlen);

ssize_t recvfrom(int sockfd, const void* buf, size_t nbytes, int flags,

struct sockaddr *addr, socklen_t *addrlen)

        sendto函数中需要指定发送的目的地址,以及目的地址的长度。因为是无连接    的套接字,所以要在发送的程序中指定目标地址。在recvfrom函数中addr保存的是发送程序所在计算机的地址,addrlen中保存地址的长度。

最后还有一对用于不连续存储缓冲区的发送接收函数:sendmsgrecvmsg

        Ssize_t sendmsg(int sockfd, const struct msghdr *mag, int flag)

        Ssize_t recvmsg(int sockfd, const struct msghdr *mag, int flag)

        成功返回以字节计数的消息长度,若无可用消息或对方已经按序结束则返回0,失败返回-1

 

服务器:

1)  建立socket描述符。和客户端一样

 

2)  绑定服务器的IP地址以及端口号。该步骤可以调用bind函数实现,但是在调用bind函数之前通常需要将主机地址和端口号赋给类型为struct sockaddr_in的变量addr中的相应成员。

int bind(int sockfd, const struct sockaddr *addr, socklen_t len)

成功则返回0,出错返回-1

       

3)  调用listen函数宣告可以接受连接请求

int listen(int sockfd, int backlog)

成功返回0,失败返回-1backlog是系统允许进入等待主机响应的队列的数量

 

4)  调用accpet函数建立连接

int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict len)

成功返回套接字描述符,失败返回-1

备注:返回的套接字描述符叫做连接描述符,参数sockfd叫做监听描述符。真正和客户端进程建立连接的套接字是返回的连接套接字,后续的发送和接受操作也是在该套接字上进行的。监听套接字描述符可以继续监听后续达到的连接请求。

 

             5)调用发送接收函数进行数据通信,和客户端中的最后一步一样,有四组接口可以调用。

 

示例程序

client.c

 

  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. #include<string.h>
  4. #include<errno.h>
  5. #include<sys/types.h>
  6. #include<sys/socket.h>
  7. #include<netinet/in.h>

  8. #define MAXLINE 4096

  9. int main(int argc, char** argv)
  10. {
  11.     int sockfd, n;
  12.     char recvline[4096], sendline[4096];
  13.     struct sockaddr_in servaddr;
  14.     
  15.     if( argc != 2)
  16.     {
  17.         printf("usage: ./client \n");
  18.         exit(0);
  19.     }

  20. /*建立套接字描述符*/
  21.     if( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
  22.     {
  23.         printf("create socket error: %s(errno: %d)\n", strerror(errno),errno);
  24.         exit(0);
  25.     }

  26. /*初始化变量servaddr,里面包括了地址族、端口号以及邋IP地址*/    
  27.     memset(&servaddr, 0, sizeof(servaddr));
  28.     servaddr.sin_family = AF_INET;
  29.     servaddr.sin_port = htons(6666);

  30.     if( inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
  31.     {
  32.         printf("inet_pton error for %s\n",argv[1]);
  33.         exit(0);
  34.     }

  35. /*调用connect函数向服务器请求建立连接*/
  36.     if( connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
  37.     {
  38.         printf("connect error: %s(errno: %d)\n",strerror(errno),errno);
  39.         exit(0);
  40.     }
  41.     printf("send msg to server: \n");

  42. /*建立连接之后使用send函数向服务器发送数据*/
  43.     fgets(sendline, 4096, stdin);
  44.     if( send(sockfd, sendline, strlen(sendline), 0) < 0)
  45.     {
  46.         printf("send msg error: %s(errno: %d)\n", strerror(errno), errno);
  47.         exit(0);
  48.     }

  49.     close(sockfd);

  50.     exit(0);
  51. }

server.c

 

  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. #include<string.h>
  4. #include<errno.h>
  5. #include<sys/types.h>
  6. #include<sys/socket.h>
  7. #include<netinet/in.h>

  8. #define MAXLINE 4096

  9. int main(int argc, char** argv)
  10. {
  11.     int listenfd, connfd;
  12.     struct sockaddr_in servaddr;
  13.     char buff[4096];
  14.     int n;

  15. /*建立套接字描述符listenfd,用于监听达到的请求*/
  16.     if( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1 )
  17.     {
  18.         printf("create socket error: %s(errno: %d)\n",strerror(errno),errno);
  19.         exit(0);
  20.     }

  21. /*初始化类型为struct sockaddr_in的变量servaddr的成员值,地址族、端口号和邋IP地址*/
  22.     memset(&servaddr, 0, sizeof(servaddr));
  23.     servaddr.sin_family = AF_INET;
  24.     servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
  25.     servaddr.sin_port = htons(6666);

  26. /*绑定主机地址和端口号*/
  27.     if( bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1)
  28.     {
  29.         printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno);
  30.         exit(0);
  31.     }

  32. /*宣告服务器允许接受连接请求*/
  33.     if( listen(listenfd, 10) == -1)
  34.     {
  35.         printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno);
  36.         exit(0);
  37.     }

  38.     printf("======waiting for client's request======\n");
  39.     while(1)
  40.     {
  41. /*接受客户进程的连接请求,不保存客户进程的地址*/    
  42.         if( (connfd = accept(listenfd, (struct sockaddr*)NULL, NULL)) == -1)
  43.         {
  44.             printf("accept socket error: %s(errno: %d)",strerror(errno),errno);
  45.             continue;
  46.         }
  47. /*调用recv函数接受客户进程发送过来的数据*/        
  48.         n = recv(connfd, buff, MAXLINE, 0);
  49.         buff[n] = '\0';
  50.         printf("recv msg from client: %s\n", buff);
  51.         close(connfd);
  52.     }
  53.     
  54.     close(listenfd);
  55. }

 

 

 

 

 

 

 

 

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