码迷,mamicode.com
首页 > 其他好文 > 详细

socket套接字编程

时间:2015-07-10 15:20:40      阅读:146      评论:0      收藏:0      [点我收藏+]

标签:socket   套接字   

socket套接字是一种网络IPC,既可以在计算机内通信,也可以在计算机间通信。socket接口可以采用许多不同的网络协议,如常见的TCP/IP协议。

1、socket描述符

类似于文件描述符,访问socket也有对应的socket描述符。要创建一个套接字,调用socket函数:

#include <sys/socket.h> 

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

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

参数domain,即域,指定通信的特性,包括地址格式。各个域有自己的格式表示地址,而表示各个域的常数都以AF_开头,意思是地址家族,代表英文单词Address和Family。在Linux上,通过man查得有如下域:

Name            Purpose                                 Man page 
AF_UNIX, AF_LOCAL     Local communication           unix(7) 
AF_INET             IPv4 Internet protocols     ip(7) 
AF_INET6            IPv6 Internet protocols     ipv6(7) 
AF_IPX          IPX - Novell protocols 
AF_NETLINK      Kernel user interface device        netlink(7) 
AF_X25          ITU-T X.25 / ISO-8208 protocol      x25(7) 
AF_AX25         Amateur radio AX.25 protocol 
AF_ATMPVC       Access to raw ATM PVCs 
AF_APPLETALK        Appletalk                               ddp(7) 
AF_PACKET           Low level packet interface          packet(7) 

POSIX.1还包括AF_UNSPEC,可以代表任何域。

参数type确定套接字的类型,进一步确定通信特征。通过man查得有如下类型:

SOCK_STREAM:Provides sequenced, reliable, two-way, connection-based  byte  streams.   An out-of-band data transmission mechanism may be supported. 
SOCK_DGRAM :Supports  datagrams  (connectionless, unreliable messages of a fixed maximum length). 
SOCK_SEQPACKET :Provides a sequenced, reliable, two-way connection-based  data  transmission path  for  datagrams of fixed maximum length; a consumer is required to read an entire packet with each input system call. 
SOCK_RAW:Provides raw network protocol access. 
SOCK_RDM:Provides a reliable datagram layer that does not guarantee ordering. 
SOCK_PACKET:Obsolete and should not be used in new programs; see packet(7). 
SOCK_NONBLOCK:Set the O_NONBLOCK file status flag on the new open file description.  Using this flag saves extra calls to fcntl(2) to achieve the same result. 
SOCK_CLOEXEC:Set the close-on-exec (FD_CLOEXEC) flag on the new file descriptor.  See the description  of  the  O_CLOEXEC  flag in open(2) for reasons why this may be useful. 

较为常见的是 SOCK_DGRAM和SOCK_STREAM,前者表示长度固定的、无连接的不可靠报文传递,后者表示有序、可靠、双向的面向连接字节流。

参数protocol通常是零,表示按给定的域和套接字类型选择默认协议。当对同一域和套接字类型支持多个协议时,可以使用protocol参数选择一个特性协议。在AF_INET通信域中套接字类型SOCK_STREAM的默认协议是TCP传输控制协议, 在AF_INET通信域中套接字类型 SOCK_DGRAM的默认协议是UDP用户数据报协议。

对于数据报接口,与对方通信时是不需要逻辑连接的,只需要送出一个报文,其地址是一个对方进程所使用的套接字,因此数据报提供了一个无连接的服务。而字节流要求在交换之前,在本地套接字和与之通信的远程套接字之间建立一个逻辑连接。

数据报是一种自包含报文,发送数据报近似于给某人邮寄邮件,可以邮寄很多信,但不能保证投递的次序,并且可能有些信件丢失在路上,每封信件包含接收者的地址,使这封信件独立于所有其它信件,每封信件可能送达不同的接收者。相比之下,使用面向连接的协议通信就像与对方打电话,首先需要通过电话建立一个连接,连接建立好之后,彼此能双向通信,每个连接是端到端的通信通道,会话中不包含地址信息,就像呼叫的两端存在一个点对点虚拟连接,并且连接本身暗含特定的源和目的地。

套接字通信是双向的,可以采用shutdown函数来禁止套接字上的输入输出,函数如下:

#include <sys/socket.h> 

int shutdown(int sockfd, int how);

参数how可以是SHUT_RD、SHUT_WR、SHUT_RDWR,分别表示关闭读、写、读写。

2、大小端字节序

字节序是一个处理器架构特性,用于指示像整数这样的大数据类型的内部字节顺序。如果处理器架构支持大端字节序,那么最大字节地址对应于数字最低有效字节,小端字节序则相反,数字最低字节对应于最小字节地址。注意不管字节如何排序,数字最高位总是在左边,最低位总是在右边。

运行在同一台计算机上的进程相互通信时,一般不用考虑字节序。网络协议指定了字节序,因此异构计算机系统能够交换协议信息而不会混淆字节序。TCP/IP协议采用大端字节序,应用程序交换格式化数据时,字节序可能就会出问题,所以需要在处理器和网络之间进行字节序转换,下面是四个转换函数:

#include <arpa/inet.h> 

uint32_t htonl(uint32_t hostlong); 
uint16_t htons(uint16_t hostshort); 
uint32_t ntohl(uint32_t netlong); 
uint16_t ntohs(uint16_t netshort); 

上面函数中,h表示主机host,n表示网络net,l表示长整型long,s表示短整型short。

3、套接字地址格式

地址标识了特定通信域中的套接字特点,地址格式与特定的通信域相关。为使不同格式地址能够被传入到套接字函数,地址被强制转换成通用的地址结构sockaddr表示:

struct sockaddr {
        sa_family_t sa_family;
        char sa_data[]; // variable length
        ... 
}

sockaddr中, sa_data长度是可变的,在Linux中为14,还可以自由地添加额外的成员。

netinet/in.h头文件中定义了IPv4因特网域AF_INET的套接字地址结构sockaddr_in:

struct in_addr {
        in_addr_t s_addr; 
}

struct sockaddr_in {
        sa_family_t sin_family;
        in_port_t sin_port;
        struct in_addr sin_addr;
}

在Linux下, sockaddr_in定义如下:

struct sockaddr_in {
        sa_family_t sin_family;
        in_port_t sin_port;
        struct in_addr sin_addr;
        unsigned char sin_zero[8];
}

sin_zero为填充字段,必须全部被置为0。

有时候需打印出能被人所理解的地址格式,如函数inet_addr和inet_ntoa,用于在二进制地址格式与点分十进制字符串表示之间相互转换,但它们仅用于IPv4,功能相似的两个函数inet_ntop和inet_pton则支持IPv4和IPv6。

#include <arpa/inet.h> 

const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
int inet_pton(int af, const char *src, void *dst);

n表示网络字节序的二进制地址,p表示文本字符串格式。

4、socket地址查询

理想情况下,应用程序不需要了解套接字地址的内部结构,如果应用程序只是简单地传递类似于sockaddr结构的套接字地址,并且不依赖与任何协议相关的特性,那么可以与提供相同服务的许多不多协议协作。

计算机网络配置信息可能存放在许多地方,无论这些信息放在何处,调用相关函数都能够访问它们。通过调用gethostent函数,便可以找到给定计算机的主机信息。

 #include <netdb.h>

struct hostent *gethostent(void);
void sethostent(int stayopen);
void endhostent(void);

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 */ 
} 

如果主机数据文件没有打开,gethostent会打开它,返回文件的下一个条目,返回类型为指向hostent结构的指针,该结构可能包含一个静态的数据缓冲区,每次调用都会覆盖这个缓冲区,返回的地址采用网络字节序。函数endhostent将关闭文件。除了上面提到的几个函数,还有gethostbyname和gethostbyaddr有类似的功能,不过它们可能被认为过时而替换为如下函数:

#include <netdb.h> 

struct netent *getnetent(void); 
struct netent *getnetbyname(const char *name); 
struct netent *getnetbyaddr(uint32_t net, int type); 
void setnetent(int stayopen); 
void endnetent(void);

struct netent { 
    char      *n_name;     /* official network name */ 
    char     **n_aliases;  /* alias list */ 
    int        n_addrtype; /* net address type */ 
    uint32_t   n_net;      /* network number */ 
}

协议名字和协议号采用下列函数映射:

#include <netdb.h> 

struct protoent *getprotoent(void); 
struct protoent *getprotobyname(const char *name); 
 struct protoent *getprotobynumber(int proto); 
void setprotoent(int stayopen); 
void endprotoent(void); 

struct protoent { 
    char  *p_name;       /* official protocol name */ 
    char **p_aliases;    /* alias list */ 
    int    p_proto;      /* protocol number */ 
}

服务是由地址的端口号部分表示的,每个服务由一个唯一的、熟知的款口号来提供。采用函数getservbyname可以将一个服务名字映射到一个端口号,函数getservbyport将一个端口号映射到一个服务名,或者采用函数getservent顺序扫描服务数据库。

#include <netdb.h> 

struct servent *getservent(void); 
struct servent *getservbyname(const char *name, const char *proto); 
struct servent *getservbyport(int port, const char *proto); 
void setservent(int stayopen); 
void endservent(void);

struct servent { 
    char  *s_name;       /* official service name */ 
    char **s_aliases;    /* alias list */ 
    int    s_port;       /* port number */ 
    char  *s_proto;      /* protocol to use */ 
}

函数getaddrinfo允许将一个主机名字和服务名字映射到一个地址:

#include <netdb.h> 

int getaddrinfo(const char *node, const char *service, 
        const struct addrinfo *hints, 
        struct addrinfo **res); 
void freeaddrinfo(struct addrinfo *res); 
 const char *gai_strerror(int errcode);

struct addrinfo { 
    int              ai_flags; 
    int              ai_family; 
    int              ai_socktype; 
    int              ai_protocol; 
    socklen_t        ai_addrlen; 
    struct sockaddr *ai_addr; 
    char            *ai_canonname; 
    struct addrinfo *ai_next; 
}; 

freeaddrinfo用来释放一个或多个addrinfo结构,出错时不能使用perror函数或者strerror函数来生成错误消息,要用上面的gai_strerror函数。

函数getnameinfo则将地址转换成主机名或者服务名:

#include <sys/socket.h> 
#include <netdb.h> 

int getnameinfo(const struct sockaddr *sa, socklen_t salen, 
        char *host, size_t hostlen, 
        char *serv, size_t servlen, int flags);

5、绑定套接字与地址

绑定地址到套接字使用函数bind:

 #include <sys/socket.h> 

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

调用函数getsockname可以发现绑定到一个套接字的地址,如果套接字已经和对方连接,则调用getpeername函数。

#include <sys/socket.h> 

int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

6、建立连接

如果处理的是面向连接的网络服务,如SOCK_STREAM,在开始交换数据以前,需要在客户端和服务器之间建立一个连接,调用connect函数。

#include <sys/socket.h> 

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 

在connect中所指定的地址是想与之通信的服务器地址,如果sockfd没有绑定到一个地址,connect会给调用者绑定到一个默认地址。connect时有可能失败,下面的例子使用了指数补偿的算法进行连接。

#include <stdio.h> 
#include <sys/socket.h> 

#define MAXSLEEP 128 

int connect_retry(int sockfd, const struct sockaddr *addr, socklen_t alen) 
{ 
    int nsec; 
    for (nsec = 1; nsec <= MAXSLEEP; nsec <<= 1) { 
        if (0 == connect(sockfd, addr, alen)) { 
            return 0; 
        } 
        if (nsec <= MAXSLEEP / 2) { 
            sleep(nsec); 
        } 

    } 
    return -1; 
}

connect成功后,服务器调用listen开始监听,宣告可以接受连接请求。

#include <sys/socket.h> 

int listen(int sockfd, int backlog); 

参数backlog表示同时可接受的最大请求数量,这个值有上限,依据系统而定。如果请求队列已满,系统会拒绝多余连接请求。
一旦服务器调用了listen,套接字就能接收连接请求,使用accept函数获得连接请求并建立连接。

#include <sys/socket.h> 

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 

accept失败返回-1,成功返回套接字描述符,该描述符连接到调用connect的客户端,并且和原始套接字,即accept参数sockfd具有相同的套接字类型和地址族。原始套接字没有关联到这个连接,而是继续保持可用状态并接受其它连接请求。accept是个阻塞函数,如果服务器调用accept并且当前没有连接请求,服务器会阻塞直到一个请求到来。

7、数据传输

数据传输有下列几个函数:

#include <sys/socket.h> 

ssize_t send(int sockfd, const void *buf, size_t len, int flags); 
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, 
        const struct sockaddr *dest_addr, socklen_t addrlen); 
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

ssize_t recv(int sockfd, void *buf, size_t len, int flags); 
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, 
        struct sockaddr *src_addr, socklen_t *addrlen); 
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags); 

send和recv用于面向连接的套接字,sendto和recvfrom用于无连接的套接字,sendmsg和recvmsg用来指定多重缓冲区传输数据。上面函数的flags参数有以下几个选项:

MSG_OOB:如果协议支持,接收带外数据。
MSG_PEEK:返回报文内容而不真正取走报文。
MSG_TRUNC:即使报文被截断,要求返回的是报文的实际长度。
MSG_WAITALL:等待直到所有的数据可用,仅SOCK_STREAM。
MSG_CTRUNC:控制数据被截断。
MSG_DONTWAIT:recvmsg处于非阻塞模式。
MSG_EOR:接收到记录结束符。

8、总结

面向连接(TCP)的服务器步骤:
(1)创建套接字socket。
(2)把地址绑定到套接字bind。
(3)监听连接listen。
(4)接收客户端的连接accept。
(5)数据传输recv、send。
(6)结束。

面向连接(TCP)的客户端步骤:
(1)创建套接字socket。
(2)连接服务器connect。
(3)数据传输recv、send。
(4)结束。

无连接(UDP)的服务器步骤:
(1)创建套接字socket。
(2)把地址绑定到套接字bind。
(3)数据传输recvfrom、sendto。
(4)结束。

无连接(UDP)的客户端步骤:
(1)创建套接字socket。
(3)数据传输recvfrom、sendto。
(3)结束。

9、例子

下面是一个TCP&IP面向连接的例子,客户端向服务器发送一个字母(没有做输入合法性验证),服务器判断字母大小写,并进行大小写转换,转换结果再发送给客户端,客户端输入数字0时结束,运行客户端时要通过命令行参数指定要连接的服务器IP。

// client
// character convertion between upper and lower

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define SOCKPORT 8000

int main(int argc, char** argv)
{
    int sockfd;
    struct sockaddr_in servaddr;
    char sendletter;
    char recvletter;

    if (argc != 2) {
        printf("usage: ./client <server_ip>\n");
        return -1;
    }

    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        printf("socket error: %s\n", strerror(errno));
        return -1;
    }

    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SOCKPORT);
    if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) {
        printf("inet_pton error: %s, ip is %s\n", strerror(errno), argv[1]);
        close(sockfd);
        return -1;
    }

    if (connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1) {
        printf("connect error: %s\n", strerror(errno));
        close(sockfd);
        return -1;
    }

    while (1) {
        printf("\ninput a character:\n\talphabet - convert between upper and lower\n\tdigit 0 - quit\n");
        printf("\n\tyour characer: ");
        scanf("%c", &sendletter);
        getchar();

        if (strncmp(&sendletter, "0", 1) == 0) {
            printf("quit\n");
            break;
        }

        if (send(sockfd, &sendletter, sizeof(sendletter), 0) == -1) {
            printf("send error: %s\n", strerror(errno));
            break;
        }

        if (recv(sockfd, &recvletter, sizeof(recvletter), 0) == -1) {
            printf("recv error: %s\n", strerror(errno));
            break;
        }

        printf("\tfrom [%c] to [%c]\n", sendletter, recvletter);
    }

    close(sockfd);
    return 0;
}

// server
// character convertion between upper and lower

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define SOCKPORT 8000

int main(int argc, char** argv)
{
    int sockfd;
    int clifd;
    char srcletter;
    char desletter;
    struct sockaddr_in servaddr;

    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        printf("socket error: %s\n", strerror(errno));
        return -1;
    }

    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SOCKPORT);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // INADDR_ANY native ip

    if (bind(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1) {
        printf("bind error: %s\n",strerror(errno));
        close(sockfd);
        return -1;
    }

    if (listen(sockfd, 1) == -1) {
        printf("listen error: %s\n", strerror(errno));
        close(sockfd);
        return -1;
    }
    printf("===== waiting for client‘s request =====\n");

    if((clifd = accept(sockfd, (struct sockaddr*)NULL, NULL)) == -1) {
        printf("accept error: %s\n", strerror(errno));
        close(sockfd);
        return -1;
    }

    while (1) {
        if (recv(clifd, &srcletter, sizeof(srcletter), 0) == -1) {
            printf("recv error: %s\n", strerror(errno));
            break;
        }

        if (isupper(srcletter)) {
            desletter = tolower(srcletter);
        }
        else if (islower(srcletter)) {
            desletter = toupper(srcletter);
        }
        printf("recv [%c] send[%c]\n", srcletter, desletter);

        if (send(clifd, &desletter, sizeof(desletter), 0) == -1) {
            printf("send error: %s\n", strerror(errno));
            break;
        }
    }

    close(clifd);
    close(sockfd);
    return 0;
}

版权声明:本文为博主原创文章,未经博主允许不得转载。

socket套接字编程

标签:socket   套接字   

原文地址:http://blog.csdn.net/ieearth/article/details/46829649

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!