一、TCP/IP与Socket介绍

TCP/IP(Transmission Control Protocol/Internet Protocol)即传输控制协议/网间协议,是一个工业标准的协议集,它是为广域网(WANs)设计的。UDP(User Data Protocol,用户数据报协议)是与TCP相对应的协议。它是属于TCP/IP协议族中的一种。下面是他们三者的关系:
image.jpeg
图 TCP与Socket在网络传输的位置
可以看出TCP/IP协议族包括运输层、网络层、链路层。socket是一个接口,在用户进程与TCP/IP协议之间充当中间人,完成TCP/IP协议的书写,用户只需理解接口即可。
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
image.png

二、Socket 常用API (Linux/Unix系统)

1、socket() 创建socket描述符

  1. int socket(int domain, int type, int protocol); //成功返回非负描述符,失败返回-1
  2. domain:即协议域,又称为协议族(family
  3. domain:即协议域,又称为协议族(family
  4. AF_INET //IPV4地址
  5. AF_INET6 //IPV6地址
  6. AF_LOCAL //AF_UNIX,本地通信用,在内核中通信
  7. AF_LOCAL //AF_UNIX

表 Socket domain
image.png
协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。

  1. type:信息传送方式
  2. SOCK_STREAM //数据流socket:提供可靠的、双向的字节流通信信道
  3. SOCK_DGRAM //数据报socket:保留数据边界,传输不可靠,数据可能无序或丢失
  4. SOCK_RAW
  5. SOCK_RAW
  6. SOCK_PACKET
  7. protocol:对应协议。
  8. IPPROTO_TCP TCP传输协议
  9. IPPTOTO_UDP UDP传输协议
  10. IPPROTO_SCTP STCP传输协议
  11. IPPROTO_TIPCTIPC传输协议

注意:通常设置为0,让其自动匹配。
我的理解就是:
domain 网络层相关协议 type 信息传送方式 protocol 运输层相关协议,如果前两个都确定了,系统就可以自动选择协议了。

2、bind()绑定实际地址

  1. int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  2. //返回值:成功则为0,失败为-1
  3. sockfd 一般服务端才需要绑定,客户端由系统内核解决
  4. addr 所有协议都有一个公共的结构叫做const struct sockaddr,不同协议对应不同的具体结构,结构如下:
  5. sockfd 一般服务端才需要绑定,客户端由系统内核解决
  6. sa_family_t sin_family;//地址族
  7.    char sa_data[14]; //14字节,包含套接字中的目标地址和端口信息
  8.    };

可以看出,我们端口和目标地址放在同一数组中,不太容易使用。对于不同的协议,我们会通过另一种特定的结构体的来完成初始化,再通过强制类型转换,来使用bind函数。
强制转换第二个地址参数为const sockaddr *

  1. (const sockaddr *)sockaddr_in;
  2. (const sockaddr *)sockaddr_in6;
  3. (const sockaddr *)sockaddr_un;

ipv4结构体(最常用)

struct sockaddr_in {
    sa_family_t sin_family;
    in_port_t sin_port;                                       //typedef    __uint16_t        in_port_t;
    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];
    };
    
  • addrlen:协议结构体大小

通常使用sizeof运算符计算

sizeof(sockaddr_in);          //IPv4 in:internet
sizeof(sockaddr_in6);        //IPv6
sizeof(sockaddr_un);         //本地 un:unix
};

3、服务端监听listen( )

int listen(int sockfd, int backlog);      //返回值:成功则为0,失败为-1

listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的 socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。

4、服务端接受链接accept ()

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

//返回值:成功则为0,失败为-1
第一个参数为服务器的socket描述字,
第二个参数为指向struct sockaddr 的指针,用于返回客户端的协议地址
第二个参数为指向
如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字代表与返回客户的TCP连接。后面的*读写操作都是根据这个返回值来完成的

5、客户端connect()链接

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);                   //返回值:成功则为0,失败为-1

connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。

6、write()和read()

write函数将buf中的nbytes字节内容写入文件描述符fd。成功时返回写的字节数。失败时返回-1.。并设置errno变量。在网络程序中,当我们向套接字文件描述符写时有两可能。
1)write的返回值大于0,表示写了部分或者是全部的数据。这样我们用一个while循环来不停的写入,但是循环过程中的buf参数和nbyte参数得由我们来更新。也就是说,网络写函数是不负责将全部数据写完之后在返回的。
2)返回的值小于0。此时出现了错误,我们要根据错误类型来处理。
如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接)。
为了处理以上的情况,可以自己编写一个写函数来处理这几种情况。
ssize_t read(int fd,void *buf,size_t nbyte)
read函数是负责从fd中读取内容.当读成功 时,read返回实际所读的字节数,如果返回的值是0 表示已经读到文件的结束了,小于0表示出现了错误.如果错误为EINTR说明读是由中断引起 的,如果是ECONNREST表示网络连接出了问题。

7、send()和recv()

int send(nt sockfd,void *buf,int len,int flags)
int recv(int sockfd,void *buf,int len,int flags)

前面的三个参数和read,write一样,第四个参数可以是0或者是以下的组合

MSG_DONTROUTE 不查找表
MSG_OOB 接受或者发送带外数据
MSG_PEEK 查看数据,并不从系统缓冲区移走数据
MSG_WAITALL 等待所有数据

MSG_DONTROUTE:是send函数使用的标志。这个标志告诉IP。目的主机在本地网络上面,没有必要查找表。这个标志一般用网络诊断和路由程序里面。
MSG_OOB:表示可以接收和发送带外的数据。
MSG_PEEK:是recv函数的使用标志,表示只是从系统缓冲区中读取内容,而不清除系统缓冲区的内容。这样下次读的时候,仍然是一样的内容.一般在有多个进程读写数据时可以使用这个标志。
MSG_WAITALL是recv函数的使用标志,表示等到所有的信息到达时才返回。使用这个标志的时候recv回一直阻塞,直到指定的条件满足,或者是发生了错误。
1)当读到了指定的字节时,函数正常返回。返回值等于len
2)当读到了文件的结尾时,函数正常返回。返回值小于len
3)当操作发生错误时,返回-1,且设置错误为相应的错误号(errno)。

8、close()关闭服务

int close(int fd);                       //返回值:成功则为0,失败为-1

close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read、write或send、recv的第一个参数。
注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。