zoukankan      html  css  js  c++  java
  • Linux编程---套接字

    网络相关的东西差点儿都是建立在套接字之上.所以这个内容对于程序猿来说还是蛮重要的啊.

    事实上套接字也就是一个特殊的设备文件而已,我始终不能明确为什么要叫套接字.这么个奇怪的名字.只是还是就这样算了吧.关键还是编程上.因为其重要性,我还是写的具体一点吧.

    .套接字

    核心函数: int  socket(int domain,int type,int protocol);

    这个函数在通信域domain中创建一个类型为type,使用协议protocol的套接字.而且返回一个描写叙述字,也就是相当于打开了一个特殊的文件.能够用close,read,write等函数来操纵这个文件.当中domain预计对于仅仅搞过java编程的预计都不知道是什么吧.最常见的有例如以下几种:

    AF_UNIX: 这个指的是UNIX通信域,即同一台计算机的两个不同进程.也就是我在其它文章中,为进程间的通信用的socket.

    AF_INET: 使用IPv4协议,这个用的就是我们常见的32位的IPv4地址

    AP_INET6: 使用IPv6协议,要求的IP地址为128位的IPv6地址

    事实上这个一般应用都填0,由系统自己主动的选择默认协议.

    然后是对于type的指定,主要有以下几种:

    SOCK_STREAM: 流套接字,也就是所谓的TCP

    SOCK_DGRAM: 数据报套接字,也就是所谓的UDP

    SOCK_RAW: 这个能够越过高层协议直接訪问底层协议,让程序猿能够直接使用IP协议或网络的物理层.查看现存协议的实现细节就能够用这个.

    而且对于这三个并非能够随便搞的.第三个对于INTINT6都是相应的IPv4IPv6.但对于UNIX来说,这个就是没意义的.

    对于第三个參数,事实上仅仅用写0就能够了.直接匹配默认协议.详细协议,用到后再查吧.

    还有第二种一次穿件两个socket的函数 socketpair.參数添加一个大小为2的整数数组,于返回两个文件描写叙述符.这个使用方法和管道非常像.

    关闭:

    这里关闭分为两种,一种是直接关闭这个描写叙述字,还有一种是断开其链接,

    第一种用close大家都非常熟悉,另外一种用int shutdown(int socket,int how)

    当中怎样断开链接有以下三种方法:

    SHUT_RD: 停止从套接字接收数据

    SHUT_WR: 停止套接字传送数据

    SHUT_RDWR: 全然停止.

    二.套接字地址的结构

    在系统中,32位的套接字就是个无符号整形变量.可是为了人能识别清楚,所以有了点分十进制记法的IP地址记法.所以就衍生出一些转换函数

    inet_aton   inet_ntoa   inet_pton   inet_ntop

    a表示IPv4的点分十进制,p表示的是IPv6的点分十进制.n表示二进制地址.

    所以这四个函数各自是IPv4字符-二进制,二进制-IPv4字符,IPv6字符-二进制,二进制-IPv6字符.

    而且另一些特殊的地址记录在<netinet/in.h>

    INADDR_LOOPBACK: 这个表示的是自己本机的地址,也就是回送地址,127.0.0.1

    INADDR_ANY: 通配名,表示不论什么进入本机的地址
    INADDR_BROADCAST: 广播地址,也就是255.255.255.255

    还有,因为不同主机有不同的IP地址,即便是点分十进制也是非常难记住的.不如一个有意义的字符串好记.这个字符串就是我们常说的域名.学过计算机网络的应该就非常清楚了,域名->DNS或本地域名系统->IP->主机->对方server.

    详细的函数就是

    struct hostent * gethostbyname(const char *name);

    struct hostent * gethostbyaddr(const void *addr,size_t length,int type);

    两个都是返回一个关于地址的结构体.当中包括:

    主机的正式名字: char *h_name

    主机的可选别名: char ** h_aliases

    主机地址类型: int h_addrtype

    每一个地址的长度: int h_length;对于IPv44,IPv616

    指向主机网络地址数组的指针: char ** h_addr_list;因为对方站点可能有多个IP,所以用list

    第一个通过DNS或本地域名系统来訪问.假设是DNS用的是UDP来请求.

    第二个则是第一个參数是二进制的结构,不是字符串.可是我就搞不明确..为什么要一个size和一个type.写一个还有一个不就清楚了吗?有了解的高手能够告诉我吗~

    当须要读多个主机的地址信息时,能够先用sethostent打开主机地址数据库,然后用gethostent来逐个扫描登记项,再用endhostent来关闭数据库.

    一定注意,这几个函数是不可重入的函数.一定要保证各个线程不会同一时候启动这几个函数.

    这里的主机地址数据库我想就是本地域名系统吧.有时间关闭測试一下.

    关于port号:

    每一个套接字的地址都是由IP+PORT组成的.假设说IP是主机的身份认证,那么port就是进程的认证.标准port号小于IPPORT_RESERVED(通常为1024),仅仅有根用户才可以运行的那些服务程序才干使用它们.这样就行防止普通用户从标准port接受数据来获取他人信息.而对于应用程序来说,一般port号大于IPPORT_USERRESERVED(linux5000).

    假设使用的是没有指定地址的套接字时,系统为它自己主动生成一个应用port号.非常多client就是这么搞的,可是server端就不行了.

    主要函数有:
    struct servent *getservbyname(const char *name,const char *proto);

    struct servent *getservbyport(int port,const char *proto);

    void setservent(int stayopen);

    struct servent *getservent(void);

    void endservent(void);

    UNIX系统有一个记录标准服务的数据库,这个数据库由文件/etc/services或域名server提供.定义在<netdb.h>中的结构体servent用于表示服务数据库的登记项信息.

    servent结构有例如以下成员:
    服务程序的正式名字: char * s_name

    server别名: char *s_aliases

    服务程序相应的端口号: int s_port

    与该服务一起使用的协议名: char *s_proto

    所以简单来说,对于主机上的每一个port执行什么程序都在这个数据库中.

    setservent则是开启这个server,必须开启才干使用这些函数.

    getservbyname用来通过协议和名字来确定其servent.getservbyport则通过端口.

    getservent则是依照顺序一个个的读出数据库中的servent.

    endservent就是关闭这个服务数据库.

    套接字地址数据结构:

    1.对于进程通信的

    struct sockaddr_un{
    sa_family_t sun_family; //这是地址族仅仅能是AF_UNIX

    char sun_path[108]; //Linux下是108

    }

    2.IPv4IPv6

    sockaddr_in{
    sa_family_t sin_famili; //AF_INET

    in_prot_t sin_port; //16位端口号,网络字节序(大端)

    struct in_addr sin_addr; //32IP地址,网络字节序

    unsigned char sin_zero[8]; //保留

    }

    sockaddr_in6{
    sa_family_t sin6_famili; //AF_INET6

    in_prot_t sin6_port; //16位端口号,网络字节序(大端)

    uint32_t  sin_flowinfo; //IPv6流标号和优先级信息,网络字节序

    struct in6_addr sin6_addr;; //128IPv6地址,网络字节序

    }

    3.通用版

    struct sockaddr{
    sa_family_t sun_family; //这是地址族仅仅能是AF_UNIX

    char sun_path[108]; //Linux下是108

    }

    这个通常要将专用的特殊地质结构类型强制为struct sockaddr类型.

    字节序:
    简单来说CPU字节序有大端有小端,为了在网上统一,所以採取大端.因为大端字节序通常比小端直观(反汇编过的都知道...),所以我觉得这么选是为了调试帧信息的.

    这里有4个函数来转换字节序:

    unint16_t htons (uint16_t hostshort);

    unint32_t htonl (uint16_t hostshort);

    unint16_t ntohs (uint16_t hostshort);

    unint32_t ntohl (uint16_t hostshort);

    当中字母的意思为h为主机,n表示网络,s代表short,l代表long

    hton表示从主机到网络.ntoh表示从网络到主机.

    已经描写叙述的非常清楚了.

    这里要注意的是,假设你通信的两个机子都是大端或者小端.字节序不改都能够,由于是同样的.但对于真正实际应用的server而言,则必须做转换,由于你不知道client究竟是大端还是小端.

    三.命名套接字

    之前的socket仅仅是创建了一个没有名字的资源,其它进程无法訪问他.所以也无法从它接受消息.仅仅有当bind给套接字绑定了port和名字后,其它进程才干找到它.

    一般server是一定要bind,可是client就不一定了.假设做一个简单的echoserver的话,那么client仅仅直接去connectserver就能够了.

    绑定函数int bind(int socket,const struct sockaddr *address,socklen_t address_len);

    socket就是socket函数返回的描写叙述字.第二个參数是通用接口!所以假设你的地址结构体用的不是这个,那么一定要有强制转换!!第三个參数是未转换前的地址结构体的大小+1.

    返回值0为正常,-1则设置errno表示出错.

    绑定之后,就是connectlisten.開始区分client和服务端了.

    四.套接字通信模式

    首先,通信模式分为两种,TCPUDP.

    前者面向连接,后者则以报文形式发送.

    下图是TCP的编程过程.

    这是UDP的编程过程.

    看完了编程模式,如今再来看详细函数是怎么运行的吧~

    五.流套接字操作

    1.connect

    原型int connect (int socket,const struct sockaddr *address,socklen_t address_len);

    首先这个是用于client的.这里的socket就是本地的socket描写叙述字,address是server端的地址信息.最后的address_len为其原先地址的长度.

    返回值0为正常,错误返回-1,并设置errno,错误条件有:
    EBADD: 參数socket不是合法的套接字描写叙述字

    EALREADY: 已经有一悬挂的连接正在被处理

    ETIMEDOUT: 建立连接的时间限已过而未能建立连接.

    ECONNREFUSED: 服务端拒绝此连接.

    EINTR: 建立连接的企图被捕获的信号所中断

    这个连接产生问题的情况比較多,我写的细一点:

    正常情况下连接肯定是正常的.假设连接超时,则返回并流产连接请求.

    假设connect在连接过程中被信号中断,那么尽管也错误返回.可是请求并不流产.连接会被异步建立.

    假设建立套接字的时候设置了O_NONBLOCK的话,假设不能被马上建立的话,那么也会和被中断的时候一样,连接异步的建立.

    针对这样的异步的情况,能够用select或者poll来查询连接是否就绪.

    2.listen

    原型int listen(int socket,int backlog);

    函数会为第一个參数建立一个连接请求的侦听队列,然后这个队列就会成为一个server套接字,也叫被动套接字.第二个參数则是用来设置其队列的最大长度的.假设设置的值大于系统规定的最大限制值时,这样的情况下,当侦听队列的连接请求超过系统的限制值时,系统会自己主动的截断backlog的值为系统最大值.假设设置为小于0,则自己主动设置为0

    当返回值为0时表示成功,失败则返回-1并设置errno.

    注意这个不能用于无连接风格的套接字!

    3.accept

    原型 int accept(int socket,struct sockaddr *address,socklen_t *address_len);用于TCP.

    这个socket必须和上面listen所绑定的套接字同样.用来处理client的请求.

    客户的地址信息通过第二个參数来得到.当然,你不想要客户地址信息设置为NULL也能够.假设客户地址的长度大于第三个參数,则客户地址会被截断.而且返回值为实际的客户地址的大小.

    而且这个函数是会堵塞的,假设没有客户请求就一直堵塞.

    当然,你也能够使用fcntl来对其设置一个O_NONBLOCK.那么就不堵塞了.

    出错返回-1并设置errno.当中有两个错误要注意,一是EWOULDBLOCK发生在对套接字设置了O_NONBLOCK标志而且没有悬挂连接的时候.简单来说就是查询的时候没有connect的请求.第二个错误是EINTR指出accept在堵塞期间被信号中断,

    4.getsocknamegetpeername函数

    int getsockname(int socket,struct sockaddr *address,socklent_t *address_len);

    int getpeername(int socket,struct sockaddt *address,sockelen_t *address_len);

    第一个函数是用来得到本地套接字地址的.

    第二个函数是用来得到对方套接字地址的.

    两个函数第二个參数都是用于填装地址信息的结构.

    当然格式还是依赖通信的协议.

    5.sendrecv函数

    简单来说就是写和读函数.

    ssize_t send(int socket,const void *buffer,size_t length,int flags);

    基本和write同样,可是最后一个參数用于指明消息传送的类型.假设为0,等价于write.

    MSG_OOB导致send发送的数据成为带外数据.这个是流套接字特有的.一般数据依照写的顺序来传送.可是怎样设置成这个,那么这个数据就会优先发送.

    MSG_DONTROUTE: 不再消息中包括路由信息.这个信息预计仅仅有杀毒软件或者路由器才关心吧..

    当然不止两个參数.其它的查手冊吧.

    其返回值是它已经发送出去的字节数.失败返回-1.而且与write一样send也是堵塞的.对于非堵塞的套接字来说,假设不能发送,则马上返回EWOULDBLOCK,表示发送失败.

    假设要传送的消息太长,或者对方连接已经断开.那么会收到一个SIGPIPE信号.通知对方已经断开.这个预计就能够用来解决client突然断电的问题吧.写之前设置一个全局变量,写之后再设置还有一个值.收到信号就依据全局变量来推断是否发送完成就可以.

    ssize_t recv(int socket,void *buffer,size_t length,int flags);

    read,最后flag也有一些相关的參数:

    MSG_PEEK: 窥视套接字上的数据而实际读出他们.即虽然buffer所指对象中填入了所请求的数据,随后的readrecv仍将读到同样的数据.

    MSG_OOB: 读外带数据

    MSG_WAITALL: 函数堵塞直至接收到的所请求的所有数据.只是在以下几种情况下,这个标志设置无效:

    出现信号

    连接被中断

    指明了MSG_PEEK

    套接字出错.

    其返回值为已经读到的buffer中数据的字节数大小.假设没有消息可接受而且对方套接字已经运行shutdown,那么返回值为0.否则返回-1而且设置errno.

    六.套接字选项

    int getsockopt(int socket,int level,int optname,void *optval,socklen_t *optlen);

    int setsockopt(int socket,int level,int optname,const void *optval,socklen_t optlen);

    一个是得到socket的设置,一个是设置socket的设置.

    level这个是用来指明选项所属的层次,这个应该说的是5层结构.一般參数有SOL_SOCKET,IPPROTO_IP,IPPROTO_TCP.

    这个在百度百科上有解释,我就不多说了.

    七.带外数据

    简单来说就是发送的优先级比普通数据优先级高.主要用于一些紧急的事情发生.

    而且TCP带外数据仅仅有一个字节,能够是随意8位的值.假设超出1个字节,仅仅发送最后一个字节的数据.接收到带外数据的一方会产生一个SIGURG的信号.假设你用的select等待数据的到来,那么select会马上返回.

    对于接受方,带外数据会冲数据中抽离出来,放到一个单独的一字节缓冲区.假设之前的带外数据没有读到的话,下一次来带外数据则会覆盖之前的数据.读此数据的方法就是用带MSG_OOB的调用recv(),recvfrom()recvmsg().后面两个能够面向两种连接方式.

    当然,TCP在接受外带数据的时候会在socket中设置标志位.这里另一种嵌入式的外带数据,SO_OOBINLINE这个选项还会告诉详细的外带数据插入在哪里.

    详细推断外带数据能够用socketmark来查询.int sockatmark(int sockfd);

    传入socket的文件描写叙述字,假设有外带数据返回1,没有返回0,出错返回-1.

    书上没有指明SO_OOBINLINE是怎样找到其位置的.以后遇到再说吧.应该知道一般的SO_OOB就能够了吧.

    八.数据包套接字

    sendtorecvfrom

    int recvfrom(int socket,void *buffer,size_t size,int flags,struct sockaddr * from ,size_t *addrlen);

    int sendto(int socket,void *buffer,size_t size,int flags,struct sockaddr *to,size_t addrlen);

    前几个參数基本上和recvsend差点儿相同.没什么太多说的.后面的參数添加了发送和接受的地址和其长度.当中recv得到的地址是发送方的.sendto得到的地址是对方的.

    其错误条件也与sendrecv一样.函数返回值是实际的读或写的值.

    这样的UDP连接较TCP有下面一些特点:

    --客户程序无需与服务建立连接

    --服务程序都不会正常终止.并不会像TCP那样返回文件里止符.

    --这样的server均採用迭代服务方式

    --使用UDP的服务程序仅用一个套接字来接受和发送请求.可是对于TCP,大多每一个连接就创一个套接字.UDP的协议层,隐含一个接收缓冲区.不同的客户程序的报文均放在这个缓冲区.调用recvfrom的时候,缓冲区依照FIFO的顺序返回给进程.

    虽说是无连接,可是也能够使用connect函数来指定一个默认的目的地.会与一般的无连接程序有下面不同:
    --对于输出操作不再须要指明目的地的IP地址和port.能够用write,send取代sendto.

    --也能够用readrecv.

    对于server来说,不太可能connect.可是对于client,调用connect()就非常自然了.

    当然,这个默认地址也是更改的,能够在address,使用AF_UNSPEC地址格式即把地址设置为随意了.

    九.超时处理

    非常多套接字的IO处理是非常有可能一直堵塞的.所以这时候就须要一个定时器来让其避免无限等待了.通常的方法有三种:

    1.调用alrm使得系统在指定的时间片到期时生成SIGALRM信号

    2.用select建立一时间片等待套接字就绪.

    3.使用SO_RCVTIMEOSO_SNDTIMEO套接字选项,这两个选项自己主动对套接字的读写设置超时处理(setsockopt函数).

    最后,我个人有个疑问.曾经学的时候没有注意到.为什么对于网络地址和port的时候须要转字节序,可是对于发送的内容sendrecv就不用转字节序?

    网上查了一下,结果例如以下:
    1.对于报文实际上是真的就是大端小端来发送的.大端的内容发送到小端仍然是大端的代码.每次假设发送字符串,那么就不存在问题.

    2.有一部分人觉得sendrecv在底层做了转换.可是结合课堂上的理论来看.内容是不可能被更改的.能更改的仅仅有报文头而已.所以内容应该仍然维持原发送者的字节序.

    3.这里对于IPPORT来说,大于一个字节,所以内序可能会改变.可是对于字符串来说就不会.所以大端小端是以字节为单位的.

    我个人对这三种解释并不惬意..第一点和第三点都感觉有点问题.

    我推測是对于大端来说.从数组A開始一直发送到A+i.那么小端接受的时候也是先接受A,然后依照自己的格式存储,一直接收到A+i.这样似乎就没问题.对于字符串来说,就是8位的,所以无所谓,可是对于整数这样的须要解释类型的数据来看,就可能出现颠倒.

    好比0x01020304这个数,在大端中为0x01020304,在小端中为0x04030201.

    如果大端先发送0x01,可是在小端中将其解释到同一位置.也就是正确地址的0x04的位置.那么终于解释出来的就乱套了.简单来说对于大于一个字节的变量来说,不同机器构造顺序不同.所以仅仅对字符串来说,能够无视大小端的存在.其它大于1字节的类型都须要变换.

    欢迎指出不足之处~

  • 相关阅读:
    【JZOJ6409】困难的图论
    学习LCT小结
    jzoj5432. 【NOIP2017提高A组集训10.28】三元组
    jzoj6367. 【NOIP2019模拟2019.9.25】工厂(factory)
    jzoj6366. 【NOIP2019模拟2019.9.25】化学(chem)
    jzoj5433. 【NOIP2017提高A组集训10.28】图
    学习拓展中国剩余定理小结
    jzoj6300. Count
    jzoj3736. 【NOI2014模拟7.11】数学题
    jzoj6276. 【noip提高组模拟1】树
  • 原文地址:https://www.cnblogs.com/mengfanrong/p/3836471.html
Copyright © 2011-2022 走看看