浅谈socket(原理、方法及实例)

socket是什么?

官方:
socket一般译为套接字,是一种独立于协议的网络编程接口。应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合。

说人话:
上面的定义初次接触socket的同学们可能会看的一头雾水。其实socket在英文中也有插座、插孔的意思,在理解了socket原理后你会发现它被命名成这个单词,其实也很形象。形象的说socket好比是一个排座,它将底层的TCP/IP给封装起来并提供统一的通讯接口。因此你不需要详细了解TCP/IP(正如你不需要了解插座的具体工作方法一样),你只要用socket提供的插口(就能通上电)就能将消息发出去。

socket的基本操作

socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。既然socket是“open—write/read—close”模式的一种实现,那么socket就提供了这些操作对应的函数接口。下面以TCP为例,介绍几个基本的socket接口函数。

socket()函数

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

socket()函数用来创建套接字,确定套接字的各种属性。本质上通过socket()函数创建了一个socket描述符(socket descriptor),它唯一标识一个socket。

domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址

type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等

protocol:指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议

注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议

bind()函数和connect()函数

// bind()
int bind(int sock, struct sockaddr *addr, socklen_t addrlen);

// connect()
int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen); 

在使用socket()创建了一个socket后,服务器端需要使用 bind()函数将socket与特定的IP地址和端口绑定起来这样经该IP地址和端口的数据会由socket处理。在客户端要使用 connect()函数来发起连接请求并建立连接。

sock:socket描述符,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
addr: 一个const struct sockaddr 结构体变量的指针
serv_addr:一个const struct sockaddr 结构体变量的指针,指向要绑定给serv_addr的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同:

ipv4对应的是:
    struct sockaddr_in {
        sa_family_t sin_family;
        in_port_t sin_port;
        struct in_addr sin_addr;
    };

    struct in_addr {
        uint32_t s_addr;
    };
ipv6对应的是:
    struct sockaddr_in6 {
        sa_family_t sin6_family;
        in_port_t sin6_port;
        uint32_t sin6_flowinfo;
        struct in6_addr sin6_addr;
        uint32_t sin6_scope_id;
    };

    struct in6_addr {
        unsigned char s6_addr[16];
    };
    
Unix域对应的是:
    #define UNIX_PATH_MAX 108

    struct sockaddr_un {
        sa_family_t sun_family;
        char sun_path[UNIX_PATH_MAX];
    };

addrlen:对应的是地址的长度。
通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。

listen()函数和accept()函数

// listen() 
int listen(int sock, int backlog); 

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

顾名思义,listen()是监听函数,accept()则是接收函数。这两个函数是在服务端使用bind()发起连接请求后,使用 bind() 绑定套接字后,还需要使用 listen() 函数让套接字进入被动监听状态,再调用 accept() 函数,就可以随时响应客户端的请求了。

backlog:可以用来指定socket处理缓冲区的长度,一般20即可,也可以设置为'SOMAXCONN'(由系统来决定请求队列长度,这个值一般比较大,可能是几百,或者更多)。

当socket正在处理客户端请求时,如果有新的请求进来,socket是没法处理的,只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。这个缓冲区,就称为请求队列(Request Queue)。当请求队列满时,就不再接收新的请求,对于 Linux,客户端会收到'ECONNREFUSED' 错误。

accept():当socket处于监听状态时,可以通过 accept()函数来接收客户端请求。需要特别强调的是listen()函数 只是让套接字处于监听状态,并没有接收请求。接收请求需要使用 accept() 函数。至此服务器与客户已经建立好连接了实现了网咯中不同进程之间的通信才已经初步实现了一次通信,可以调用网络I/O进行读写操作了。

读取写入类函数(IO)

// write()/read()
ssize_t write(int fd, const void *buf, size_t nbytes);
ssize_t read(int fd, void *buf, size_t nbytes);

// send()/recv()
ssize_t send(int fd, const void *buf, size_t nbytes ,int flags);
ssize_t recv(int fd, void *buf, size_t nbytes ,int flags);

// writev()/readv()
ssize_t writev(int fd, const void *buf, size_t nbytes);
ssize_t readv(int fd, void *buf, size_t nbytes);

// sendmsg()/recvmsg()
ssize_t sendmsg(int fd, const void *buf, size_t nbytes, int flags);
ssize_t recvmsg(int fd, void *buf, size_t nbytes, int flags);

// sendto()/recvfrom()
ssize_t sendto(int fd, const void *buf, size_t nbytes ,int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int fd, void *buf, size_t nbytes ,int flags, struct sockaddr *src_addr,  socklen_t *addrlen);


fd: 要写入的文件的描述符
buf: 要写入的数据的缓冲区地址
nbytes: 要写入的数据的字节数
flags: 一般为0
dest_addr:

Linux不区分socket和其他普通文件,使用write()可以向socket中写入数据,使用 read() 可以从socket中读取数据。而两台计算机之前通信就相当于两个socket之前通信,具体是在服务器端用 write() 写入数据,客户端就能收到,然后再使用 read() 读取,这就完成了一次通信。

close()函数

(待完成)

socket缓冲区以及阻塞模式

(待完成)

实例详解

(待完成)

拓展延伸

(待完成)

个人小技巧

(待完成)
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值