Skip to content

网络套接字函数

socket模型创建流程图

socket API如下图所示:

socket函数

c
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>

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

函数参数

domain:协议版本

  • AF_INET:这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址
  • AF_INET6:与上面类似,不过是来用IPv6的地址
  • AF_UNIX:本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用

type:协议类型

  • SOCK_STREAM:这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,这个socket是使用TCP来进行传输。
  • SOCK_DGRAM:这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。
  • SOCK_SEQPACKET:该协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的接受才能进行读取。
  • SOCK_RAW:socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使用该协议)
  • SOCK_RDM:这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序

protocol:传0 表示使用默认协议。

返回值

  • 成功:返回指向新创建的socket的文件描述符
  • 失败:返回-1,设置errno

socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据,如果socket()调用出错则返回-1。对于IPv4,domain参数指定为AF_INET。对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议。如果是UDP协议,则type参数指定为SOCK_DGRAM,表示面向数据报的传输协议。protocol参数的介绍从略,指定为0即可。

bind函数

c
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>

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

函数参数

  • sockfd:socket文件描述符

  • addr:通过struct sockaddr结构体创建出一个结构体变量后构造出IP地址和端口号,ip地址及端口号需要进行大端模式(网络字节序)转换htonlhtons

    • c
        struct sockaddr {
          sa_family_t sa_family; /* address family, AF_xxx */
          char sa_data[14];/* 14 bytes of protocol address */
        };
        
        struct sockaddr_in {
            sa_family_t sin_family;  /* address family: AF_INET */
            in_port_t sin_port;      /* port in network byte order */
            struct in_addr sin_addr; /* internet address */
        };
        /* Internet address. */
        struct in_addr {
            uint32_t s_addr; /* address in network byte order */
        };
    • 创建结构体变量的时间先创建struct sockaddr_in类型的结构体,在使用的时间通过强制转换转换成struct sockaddr *类型的结构体。

    • c
        struct sockaddr_in serv;
        serv.sin_family = AF_INET;
        serv.sin_port = htons(8888);
        //serv.sin_addr.s_addr = htonl(INADDR_ANY);
        //INADDR_ANY: 表示使用本机任意有效的可用IP
        
        inet_pton(AF_INET, "127.0.0.1", &serv.sin_addr.s_addr);
  • addrlensizeof(addr)长度也就是addr变量的占用内存的大小。

函数返回值

成功返回0,失败返回-1,设置errno

服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号。

bind()的作用是将参数sockfdaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号。前面讲过,struct sockaddr *是一个通用指针类型,addr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。如:

c
struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(6666);

首先将整个结构体清零,然后设置地址类型为AF_INET网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址,端口号为6666。

listen函数

c
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>

int listen(int sockfd, int backlog);

函数描述

将套接字由主动态变为被动态

函数参数

  • sockfd:socket文件描述符
  • backlog:排队建立3次握手队列和刚刚建立3次握手队列的链接数和(同时请求连接的最大个数,还未建立连接)

查看系统默认backlog

cat /proc/sys/net/ipv4/tcp_max_syn_backlog

典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的accept()返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未accept的客户端就处于连接等待状态,listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接待状态,如果接收到更多的连接请求就忽略。listen()成功返回0,失败返回-1

listen会将sockfd内部的读写缓冲区擦除,取而代之的是建立两个队列,一个是未连接队列,一个是已连接队列。

其中,客户端只发送了一次SYN请求的就会被放在未连接队列中;当客户端收到服务端的回复后发起了第三次握手就会被移到已连接队列中。

DDOS攻击就是使用大量客户端只发起一次SYN请求而不管后续事宜,使得服务端中未连接队列满载,这样正常的客户端就无法对服务端发起建立连接的请求。

accept函数

accept函数是一个阻塞函数,若没有新的连接请求,则一直阻塞。

从已连接队列中获取一个新的连接,并获得一个新的文件描述符, 该文件描述符用于和客户端通信。(内核会负责将请求队列中的连接拿到已连接队列中)

c
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>

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

函数描述

获得一个连接,若当前没有连接则会阻塞等待

函数参数

  • sockdf:socket文件描述符
  • addr:传出参数,返回链接客户端地址信息,含IP地址和端口号
  • addrlen参数(值-结果),传入sizeof(addr)大小,函数返回时返回真正接收到地址结构体的大小,先sizeof(addr),然后传入其地址(因为也做传出参数,所以需要传地址)

函数返回值

  • 成功返回一个新的socket文件描述符,用于和客户端通信
  • 失败返回-1,设置errno

三方握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。addr是一个传出参数,accept()返回时传出客户端的地址和端口号。addrlen参数是一个传入传出参数(value-result argument),传入的是调用者提供的缓冲区addr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给addr参数传NULL,表示不关心客户端的地址。

我们的服务器程序结构是这样的:

c
while (1) {
    cliaddr_len = sizeof(cliaddr);
    connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
    n = read(connfd, buf, MAXLINE);

    ......

    close(connfd);
}

整个是一个while死循环,每次循环处理一个客户端连接。由于cliaddr_len是传入传出参数,每次调用accept()之前应该重新赋初值。accept()的参数listenfd是先前的监听文件描述符,而accept()的返回值是另外一个文件描述符connfd,之后与客户端之间就通过这个connfd通讯,最后关闭connfd断开连接,而不关闭listenfd,再次回到循环开头listenfd仍然用作accept的参数。accept()成功返回一个文件描述符,出错返回-1

connect函数

c
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>

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

函数描述

连接服务器

函数参数

  • sockdf:socket文件描述符
  • addr:传入参数,指定服务器端地址信息,含IP地址和端口号
  • addrlen:传入参数,传入sizeof(addr)大小

函数返回值

成功返回0,失败返回-1,设置errno

客户端需要调用connect()连接服务器,connectbind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。connect()成功返回0,出错返回-1

读写操作函数

接下来就可以使用writeread函数进行读写操作了;除了使用read/write函数以外, 还可以使用recvsend函数。工程实践中更为常用的是使用recvsend函数。

读取数据和发送数据

cpp
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

对应recvsend这两个函数flags直接填0就可以了。

注意

如果写缓冲区已满,write也会阻塞,read读操作的时候,若读缓冲区没有数据会引起阻塞。