IP和端口

  1. IP:全称是Internet Protocol。本质是一个整数,用于表示计算机在网络中的地址。IP协议版本有两个:分别是IPV4和IPV6。IP地址用于定位网络上的主机
    1. IPV4:使用4个字节的整型数描述一个IP地址
    2. IPV6:使用16个字节的整型数描述一个IP地址
    3. IP地址的查看:
    # linux
    ifconfig
    # windows
    ipconfig
    
    1. 测试网络是否畅通:使用ping命令
  2. 端口:端口本质也是一个16位的无符号整型数,取值范围为0~65535。端口用于定位主机上的进程。

网络协议

  1. 网络协议:指的是计算机网络中互相通信的对等实体之间交换信息时所必须遵守的规则的集合。比如说传输层的TCP协议和UDP协议、网络层的IP协议、网络接口层的以太网帧协议。

字节序

  1. 字节序:字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,也就是说对于单字符来说是没有字节序问题的,字符串是单字符的集合,因此字符串也没有字节序问题。
  2. 目前在各种体系的计算机中通常采用的字节存储机制主要有两种:大端序和小端序。
    1. 小端序:又称主机字节序。数据的低位字节存储在内存的低地址,数据的高位字节存储到内存的高地址。
    2. 大端序:又称网络字节序,和小端序相反。在套接字通信的过程中,操作的数据都是大端存储的,包括接收/发送的数据、IP地址、端口
  3. 字节序相互转换的接口:
    1. 主机字节序到网络字节序:
    // 将一个短整形从主机字节序 -> 网络字节序
    uint16_t htons(uint16_t hostshort);	
    // 将一个整形从主机字节序 -> 网络字节序
    uint32_t htonl(uint32_t hostlong);
    
    // 将主机字节序的IP地址转换为网络字节序
    // 其中主机字节序以字符串表示,网络字节序IP地址以整形表示
    int inet_pton(int af, const char *src, void *dst); 
    
    // 点分十进制IP -> 大端整形,这个函数只能处理IPV4的ip地址
    in_addr_t inet_addr (const char *cp);
    
    
    1. 网络字节序到主机字节序
    // 将一个短整形从网络字节序 -> 主机字节序
    uint16_t ntohs(uint16_t netshort)
    // 将一个整形从网络字节序 -> 主机字节序
    uint32_t ntohl(uint32_t netlong);
    
    // 将大端的整形数, 转换为小端的点分十进制的IP地址        
    const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
    
    // 大端整形 -> 点分十进制IP,这个函数只能处理IPV4的ip地址,具有局限性
    char* inet_ntoa(struct in_addr in);
    

sockaddr/sockaddr_in结构体

  1. sockaddr结构体如下所示:因为不方便对这个结构体的成员进行赋值操作,因此这个结构体不常使用
typedef unsigned short int sa_family_t;
struct sockaddr
{
    sa_family_t sa_family;      // 地址族协议, ipv4
    char sa_data[14];	        // 2字节端口、4个字节IP地址、8个字节填充
};

  1. sockaddr_in结构体:经常使用
typedef uint32_t in_addr_t;
struct in_addr
{
    in_addr_t s_addr;
};
/* Type to represent a port.  */
typedef uint16_t in_port_t;

struct sockaddr_in
{
    sa_family_t sa_family;      // 地址族协议, ipv4
    in_port_t sin_port;			/* Port number.  */
    struct in_addr sin_addr;		/* Internet address.  */

    /* Pad to size of `struct sockaddr'.  填充8个字节*/
    unsigned char sin_zero[sizeof (struct sockaddr)
    		   - sizeof(sa_family_t)
    		   - sizeof (in_port_t)
    		   - sizeof (struct in_addr)];
};

常用的套接字函数

  1. 创建套接字:使用socket函数。
    1. 函数原型如下:
    #include <sys/socket.h>
    int socket (int domain, int type, int protocol)
    函数参数:
        domain:使用的地址族协议。常用的两个:
            AF_INET:使用ipv4的网络协议
            AF_INET6:使用ipv6的网络协议
        type:
            SOCK_STREAM:使用流式的传输协议
            SOCK_DGRAM:使用报式的传输协议
        protocol:一般写0,表示使用默认的协议
            SOCK_STREAM: 流式传输默认使用的是 tcp
            SOCK_DGRAM: 报式传输默认使用的 udp
    返回值:
        成功则返回用于套接字通信的文件描述符
        失败则返回-1
    
    1. 创建的套接字默认是阻塞的,可以通过socket函数的第二个参数指定SOCK_NONBLOCK选项来创建非阻塞的套接字。
  2. 将文件描述符和本地的IP和端口进行绑定,使用bind函数
    1. bind函数声明如下:
    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    函数参数:
        sockfd:用于监听的文件描述符
        addr:传入参数,要绑定的IP和端口信息需要初始化到这个结构体中,IP和端口要转换为网络字节序
        addrlen:第二个参数指向的内存大小
    返回值:成功返回 0,失败返回 - 1
    
    1. 绑定IP的时候可以指定INADDR_ANY,这个宏表示服务器可以自动选择一个IPv4地址
    2. 在客户端程序中,也可以调用bind绑定指定的IP地址和端口号与服务器通信;如果绑定0IP地址,则表示外部机器可以访问该机器上所有的IP;如果绑定0端口,则表示由操作系统随机分配一个可用的端口。
  3. 给套接字设置监听:
int listen(int sockfd, int backlog);
函数参数:
    sockfd:用于监听的文件描述符
    backlog:未处理的连接请求队列的最大长度
  1. 接受客户端的连接请求,建立新的连接,会得到一个新的文件描述符(通信的)。这些操作可以由accept函数以及accept4函数完成。
    1. 如果accept是阻塞的,有连接请求到来就会返回一个文件描述符,用于和建立连接的这个客户端通信,没有则阻塞调用accept函数的线程;如果accept是非阻塞的,无论是否有无连接请求都会返回。如果返回值大于0,则表示有新的连接到来;如果返回值为-1,则此时程序员需要判断错误码,如果错误码是EAGAIN或者EWOULDBLOCK,表示没有连接请求。如果错误码是EINTR,表示此次调用被信号中断,需要重试接收连接请求。如果错误码是EMFILE或者ENFILE表示文件描述符达到了最大限制。
    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    函数参数:
        sockfd:用于监听的文件描述符
        addr:传出参数,里边存储了建立连接的客户端的地址信息
        addrlen:传入传出参数,用于存储 addr 指向的内存大小
    
    1. accept4函数的flag参数设置为SOCK_NONBLOCK,表示创建一个非阻塞的用于通信的文件描述符
  2. 接收数据:可以使用read或者recv函数
    1. recv函数:对于阻塞socket,如果recv函数当前收不到任何数据,会阻塞当前线程执行流;对于非阻塞socket,无论recv函数是否收到数据,会立即返回,不会阻塞当前线程执行流。
    #include <unistd.h>
    ssize_t recv(int sockfd, void *buf, size_t size, int flags);
    函数参数:
        sockfd:用于通信的文件描述符,accept函数的返回值
        buf:指向一块有效内存,用于存储接受的数据
        size:第二个参数指向的内存的容量
        flags:一般不使用,指定为0
    返回值:
        大于0:实际接收的字节数
        等于 0:对方断开了连接
        -1:接收数据失败了,对于非阻塞的接收函数,需要额外判断EAGAIN或者EWOULDBLOCK以及EINTR这些错误码
        如果返回值为-1且错误码为EWOULDBLOCK,则表明当前没有数据可收
        0:对端关闭了连接
    
    1. read函数
    ssize_t read(int sockfd, void *buf, size_t size);
    
  3. 发送数据:可以使用send或者write函数
    1. send函数:对于阻塞套接字,存在两种情况,可以发送部分字节数,发送完就立刻返回了。一个字节也不发出去,会阻塞执行流;对于非阻塞socket,send无论是否发送成功,不再等待,即不阻塞当前线程执行流,立即返回;
    
    ssize_t send(int fd, const void *buf, size_t len, int flags);
    函数参数:
        fd:用于通信的文件描述符,accept函数的返回值
        buf:传入参数,要发送的字符串
        len:要发送的字符串的长度
        flags: 特殊的属性,一般不使用,指定为 0
    返回值:
        大于 0:实际发送的字节数,和参数 len 是相等的
        -1:发送数据失败了,对于非阻塞的发送函数,需要额外判断EAGAIN或者EWOULDBLOCK以及EINTR这些错误码。
        返回-1且错误码为EWOULDBLOCK表示TCP的发送窗口太小
        0:对端关闭了连接
    
    1. write函数
    ssize_t write(int fd, const void *buf, size_t len);
    
  4. 向服务器发起连接:成功连接服务器之后, 客户端会自动随机绑定一个端口。当然,客户端也可以调用bind函数绑定一个固定的端口向服务端发起连接请求
    1. 函数原型
    int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    函数参数:
        sockfd:用于监听的文件描述符
        addr:传入参数,用于指定服务器的IP和端口信息。IP和端口也需要转换为大端然后再赋值
        addrlen:用于存储 addr 指向的内存大小
    返回值:连接成功返回 0,连接失败返回 - 1
    
    1. 如果创建的套接字是非阻塞的,则connect函数也是非阻塞的。对于阻塞套接字,connect会一直等待直到有结果才会返回;对于非阻塞套接字,connect函数会立即。如果返回-1,此时需要判断错误码是否为EWOULDBLOCK/EINTR/EINPROGRESS
  5. 解析主机IP地址:可以使用getaddrinfo、gethostbyname函数
    1. 函数原型如下:
    int getaddrinfo(const char *node, const char *service,
                           const struct addrinfo *hints,
                           struct addrinfo **res);
    // 通过域名获取IP地址
    struct hostent *gethostbyname(const char *name);
    
    1. gethostbyname不是线程安全的
  6. 关闭套接字,回收套接字对应的资源
int close(int fd);
  1. 设置/获取一个套接字选项
    1. 设置套接字选项使用setsockopt函数
      1. 设置套接字选项使用setsockopt函数,其函数原型如下:
      int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
      函数参数:
          sockfd:用于监听的套接字
          level:设置端口复用需要使用 SOL_SOCKET 宏
          optname:使用下面的两个宏都可以设置端口复用
              SO_REUSEADDR
              SO_REUSEPORT
          optval:值为1,表示设置端口复用
          optlen:optval指针指向的内存大小 
      
      1. 示例:
      // 设置端口复用,在调用bind函数之前调用
      int opt = 1;
      setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
      
    2. 获取一个套接字选项使用getsockopt函数

半关闭

  1. TCP 连接只有一方发送了FIN包,另一方没有发出FIN包,仍然可以在一个方向上正常发送数据,这种状态叫做半关闭或者半连接。当四次挥手完成两次的时候,就相当于实现了半关闭,在程序中只需要在某一端直接调用shutdown()函数即可。套接字通信默认是双工的,也就是双向通信,如果进行了半关闭就变成了单工,数据只能单向流动了。
  2. 实现半关闭的函数:
// 可以有选择的关闭套接字的读/写通道
int shutdown(int sockfd, int how);
函数参数:
    sockfd:要操作的文件描述符
    how:
        SHUT_RD: 关闭文件描述符对应的读操作
        SHUT_WR: 关闭文件描述符对应的写操作
        SHUT_RDWR: 关闭文件描述符对应的读写操作
函数返回值:
    函数调用成功返回 0,失败返回 - 1

IO多路复用

查看