【Linux】socket

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

1、socket描述符

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

#include <sys/socket.h> 
int socket(int domain, int type, int protocol);

socket函数成功时返回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. IP协议的数据包接口,应用程序负责构造自己的协议首部,由于绕过了传输协议如TCP和UDP等,创建一个原始套接字时需要超级用户权限,用以防止恶意程序绕过内建安全机制来创建报文。
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. 

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

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

适用于文件描述符的一些函数同样适用于socket描述符,如read和write等。套接字通信是双向的,可以采用shutdown函数来禁止套接字上的输入、输出,函数如下:

#include <sys/socket.h> 
int shutdown(int sockfd, int how);

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

2、大小端字节序

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

运行在同一台计算机上的进程相互通信时,一般不用考虑字节序。网络协议指定了字节序,因此异构计算机系统能够交换协议信息而不会混淆字节序。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;    /* address family */
        char sa_data[];    /* variable-length address */
        ... 
}

sockaddr中, sa_data长度是可变的,在Linux中为14,还可以自由地添加额外的成员。“netinet/in.h”头文件中定义了IPv4因特网域AF_INET的套接字地址结构sockaddr_in:

struct in_addr {
        in_addr_t s_addr;    /* IPv4 address */
}

struct sockaddr_in {
        sa_family_t sin_family;    /* address family */
        in_port_t sin_port;    /* port number */
        struct in_addr sin_addr;    /* IPv4 address */
}

如上sockaddr_in结构是SUS规定的,此外,每个实现可以自由地添加额外的字段。在Linux下, sockaddr_in定义如下:

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];    /* filter */
}

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表示文本字符串格式(点分十进制字符串)。参数af为AF_INET或AF_INET6,为AF_INET时缓冲区dst需要有足够大的空间来存放32位地址,为AF_INET6时缓冲区dst需要有足够大的空间来存放128位地址,参数size指定了用以保存文本字符串的缓冲区dst的大小,为了简化工作,INET_ADDRSTRLEN定义了足够大的空间来存放表示IPv4地址的文本字符串,INET6_ADDRSTRLEN定义了足够大的空间来存放表示IPv6地址的文本字符串。

4、socket地址查询

理想情况下,应用程序不需要了解套接字地址的内部结构,如果应用程序只是简单地传递类似于sockaddr结构的套接字地址,并且不依赖与任何协议相关的特性,那么可以与提供相同服务的许多不多协议协作。计算机网络配置信息可能存放在许多地方,如“/etc/hosts”、“/etc/services”等,无论这些信息放在何处,调用相关函数都能够访问它们。通过调用gethostent函数,便可以找到给定计算机的主机信息。

 #include <netdb.h>
struct hostent *gethostbyname(const char *name);
#include <sys/socket.h>
struct hostent *gethostbyaddr(const void *addr, socklen_t len, int type);
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结构的指针,该结构可能包含一个静态的数据缓冲区,每次调用都会覆盖这个缓冲区,返回的地址采用网络字节序。sethostent会打开文件,如果文件已经被打开,那么将其回绕。函数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); 

在进程所运行的机器上,指定的地址必须有效,不能指定一个其它及其的地址。地址必须和创建套接字时的地址族所支持的格式相匹配。端口号必须不小于1024,除非该进程具有相应的特权。一般只有套接字端点能够与地址绑定,尽管有些协议允许多重绑定。调用函数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并且当前没有连接请求,服务器会阻塞直到一个请求到来。如果sockfd处于非阻塞模式,在没有连接请求等待处理时,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); 

struct msghdr {
               void         *msg_name;       /* optional address */
               socklen_t     msg_namelen;    /* size of address */
               struct iovec *msg_iov;        /* scatter/gather array */
               size_t        msg_iovlen;     /* # elements in msg_iov */
               void         *msg_control;    /* ancillary data, see below */
               size_t        msg_controllen; /* ancillary data buffer len */
               int           msg_flags;      /* flags (unused) */
};

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

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

通常,recv函数没有数据可用时会阻塞等待,当套接字输出队列没有足够空间来发送消息是send函数也会阻塞,但是在套接字非阻塞模式下,函数不会阻塞而是立即失败返回。通过两个步骤来使用异步IO:建立套接字拥有者关系,信号(SIGIO)可以被发送到合适的进程;通知套接字当IO操作不会阻塞时发信号告知。第一个步骤有三种方式:在fcntl中使用F_SETOWN命令;在ioctl中使用FIOSETOWN命令;在ioctl中使用FIOCSPGRP命令。第二个步骤有两种方式:在fcntl中使用F_SETFL命令并且设置O_ASYNC标志;在ioctl中使用FIOASYNC命令。

另外,套接字机制还提供了两个套接字选项接口来控制套接字行为,一个接口用来设置选项,另一个接口允许查询一个选项的状态,对应的函数如下。

#include <sys/types.h>
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int optname,
                      void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname,
                      const void *optval, socklen_t optlen);

套接字选项的一个应用场景:当服务器终止并尝试立即重启时,因为通常TCP的实现不允许绑定到同一个地址,所以在超时前会连接失败,不过可以使用setsockopt的SO_REUSEADDR选项来绕过这个限制,参数optval设置为一个非零的整数值(如1),表示打开指定的optname选项。

带外数据——

带外数据out-of-band是一些通信协议支持的可选特征,运行更高优先级的数据比普通数据优先传输,即使传输队列已经有数据,带外数据优先传输。TCP支持带外数据,但是UDP不支持。TCP将带外数据称为紧急数据,仅支持一个字节,还有个紧急标记,表示在普通数据流中紧急数据所在的位置。如果采用套接字选项SO_OOBINLINE,那么可以在普通数据中接收紧急数据,为帮助判断是否接收到紧急标记,可以使用函数sockatmark。

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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值