evo

自强不息,厚德载物 ii 无人驾驶fans ^_^ Making others better! Making life better!

【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;
}
阅读更多
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/iEearth/article/details/46829649
文章标签: socket 套接字
个人分类: Linux
所属专栏: Linux之美
想对作者说点什么? 我来说一句

Linux Socket

2012年04月18日 236KB 下载

linux socket、驱动、进程编程

2011年04月28日 12.5MB 下载

Linux Socket Programming

2013年11月12日 1.95MB 下载

Linux Socket Programming by Example.pdf

2012年05月23日 3.9MB 下载

linux socket 实战编程pdf及源码

2009年07月10日 6.09MB 下载

linux socket

2017年04月12日 271KB 下载

linux socket详细分析

2013年04月21日 118KB 下载

没有更多推荐了,返回首页

不良信息举报

【Linux】socket

最多只允许输入30个字

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭