Socket 编程

socket : 套接字,计算机啊之间进行通信的一种方式或者约定。

典型应用: Web服务器和Web浏览器
学习APUE + C 语言入门网Socket

Socket 也相当于一种IO,经常会使用socket 文件描述符来处理
IP Addresss

Port

8080/ 443/ 21

Protocol

TCP、UDP、FTP、IP

数据传输方式: 主要有两种

  1. SOCK_STREAM:面向连接的数据传输方式,准确无误的传输到另一台计算机,若有丢失或者损坏,可以重新发送,效率相对较慢。HTTP协议使用SOCK_STREAM
  2. SOCK_DGRAM:无连接的数据传输方式,只传输,不校验。效率高。丢失数据概率比较低。

寻址

Socket()创建套接字

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

  • af 为地址族Address Family:
描述
AF_INET IPv4
AF_INET6 IPv6
AF_UNIX Unix Domain
AF_UPSPEC 未指定
  • type 为数据传输方式,SOCK_STREAMSOCK_DGRAM
  • protocol 为协议,有IPPROTO_TCPIPPROTO_UDP 套接字类型SOCK_DGRAM默认为IPPROTO_UDP

Bind()

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

关于sockaddr:

1
2
3
4
5
struct sockaddr {
__uint8_t sa_len; /* total length */
sa_family_t sa_family; /* [XSI] address family */
char sa_data[14]; /* [XSI] addr value (actually larger) */
};

sockaddr_in:

1
2
3
4
5
6
7
8
struct sockaddr_in {
__uint8_t sin_len;
sa_family_t sin_family;// AF
in_port_t sin_port;// 16位端口
struct in_addr sin_addr;//32位IP地址
char sin_zero[8];
// 一般用0填充
};

in_addr:

1
2
3
struct in_addr {
in_addr_t s_addr;
};

in_addr_t:

1
typedef	__uint32_t	in_addr_t;	/* base type for internet address */

关于端口:0~1023 均为系统预留端口,需要权限,Web 服务器端口位80,FTP端口为21.

为什么使用sockaddr_in 而不是sockaddr
|sin_family(2)| |sin_family(2)|
|————-| |————-|
|sin_port(2)| | |
|sin_addr(4)| |sa_data(14) |
|sin_zero(8)| | |

sockaddr 将端口地址sin_zero合成到了sa_data里,在创建的时候我们很难去赋值。

bind()函数将服务器套接字与特定IP地址和端口绑定,客户端需要用connect()函数来建立连接。

Connect()

int connect(int sockfd, const struct sockaddr *, socklen_t);
客户端通过connect()与服务器建立连接,地址为服务器的地址,sockfd绑定本地一个地址。

bind()与connect() 均为返回0时成功,返回-1时失败。

Listen()

int listen(int sockfd, int backlog);
使套接字进入被动监听状态,随时接受客户端请求。

  • 请求队列:当套接字正在处理客户端请求时,如果有新的请求进来,会被放进缓冲区,带当前请求完毕后才会从缓冲区读取并处理,从而形成了在缓冲区排队。
  • 缓冲区长度有backlog决定。如果设为SOMAXCONN则有系统决定队列长度。

Accept()

套接字一旦进入listen状态,就可以接收请求,使用accept进行连接。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
若成功,返回连接到的客户端的socket的文件描述符

如果没有连接请求在等待,accept会阻塞直到一个请求到来。

Socket 数据接收与发送

套接字也是一种文件IO,可以使用write()和read()函数从套接字中读取和写入数据

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

  • fd 为文件描述符,再次就是套接字。
  • buf为写入数据的缓冲区地址。
  • nbytes为要写入数据的子节数。

成功的话返回写入的字节数,失败返回-1

ssize_t read(int fd, const void *buf, size_t nbytes);
参数同上。

另外6个函数用于交换数据

ssize_t send(int sockd, const void *buf, size_t nbytes, int flags);

类似于write()函数,此时套接字必须已经连接

  • flags:标志:很多人建议一般设为0;
flags 说明 recv send
MSG_DONTROUTE 绕过路由表查找 *
MSG_DONTWAIT 允许非阻塞操作 * *
MSG_OOB 如果支持协议,发送或接受外带数据 * *
MSG_PEEK 返回数据包而不真正取走数据包(窥探) *
MSG_WAITALL 等待所有数据 *

关于recv()与send()函数详解见Linux send 和 recv详解

ssize_t sendto(int sockfd, const void *buf, size_t nbytes, int flags, const struct sockaddr *destaddr, socklen_t destlen);
对于面相连接的套接字,目标地址一般都是被忽略的,因为连接中隐藏了目标地址,对于无连接的套接字,除非先调用connect设置目标地址,否则不能使用send,sendto提供了发送报文的另一种方式。

ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

msghdr结构:

1
2
3
4
5
6
7
8
9
struct msghdr {
void *msg_name; /* [XSI] optional address */
socklen_t msg_namelen; /* [XSI] size of address */
struct iovec *msg_iov; /* [XSI] scatter/gather array */
int msg_iovlen; /* [XSI] # elements in msg_iov */
void *msg_control; /* [XSI] ancillary data, see below */
socklen_t msg_controllen; /* [XSI] ancillary data buffer len */
int msg_flags; /* [XSI] flags on received message */
};

ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags);
接收数据。若标志设置为MSG_PEEK时,可以查看下一个要读取的数据但不能真正取走它,当再次调用read或者recv时,才会返回查看的数据。

ssize_t recvfrom(int sockfd, void *restrict buf, size_t len, int flags, struct sockaddr *restrict addr, socklen_t *restrict addrlen);

常用于无连接的套接字。

msghdr结构的recv

ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

Socket 缓冲区

数据并不是直接通过IO程序写入网络,而是通过先写入缓冲区,然后有TCP协议族从缓冲区发送到目标机器。

IO缓冲区特性:

  • IO缓冲区在每个TCP套接字中单独存在。
  • IO缓冲区在创建套接字时自动生成。
  • 即使关闭套接字也会继续传送输出缓冲区的数据。
  • 关闭套接字会失去输入缓冲区的数据。

阻塞模式

  • TCP协议向网络发送数据时,输出缓冲区会被锁定,不允许写入,write和send会被阻塞,直到数据发送完毕。

  • 直到所有数据被写入缓冲区,write/send 才能返回

当使用 read()/recv() 读取数据时:

  • 首先会检查缓冲区,如果缓冲区中有数据,那么就读取,否则函数会被阻塞,直到网络上有数据到来。
  • 如果要读取的数据长度小于缓冲区中的数据长度,那么就不能一次性将缓冲区中的所有数据读出,剩余数据将不断积压,直到有 read()/recv() 函数再次读取。
  • 直到读取到数据后 read()/recv() 函数才会返回,否则就一直被阻塞。

TCP 三次握手连接

最后需要说明的是,发送端只有在收到对方的 ACK 确认包后,才会清空输出缓冲区中的数据。

四次握手断开连接

关于 TIME_WAIT 状态的说明

客户端最后一次发送 ACK包后进入 TIME_WAIT 状态,而不是直接进入 CLOSED 状态关闭连接,这是为什么呢?

TCP 是面向连接的传输方式,必须保证数据能够正确到达目标机器,不能丢失或出错,而网络是不稳定的,随时可能会毁坏数据,所以机器A每次向机器B发送数据包后,都要求机器B”确认“,回传ACK包,告诉机器A我收到了,这样机器A才能知道数据传送成功了。如果机器B没有回传ACK包,机器A会重新发送,直到机器B回传ACK包。

客户端最后一次向服务器回传ACK包时,有可能会因为网络问题导致服务器收不到,服务器会再次发送 FIN 包,如果这时客户端完全关闭了连接,那么服务器无论如何也收不到ACK包了,所以客户端需要等待片刻、确认对方收到ACK包后才能进入CLOSED状态。那么,要等待多久呢?

数据包在网络中是有生存时间的,超过这个时间还未到达目标主机就会被丢弃,并通知源主机。这称为报文最大生存时间(MSL,Maximum Segment Lifetime)。TIME_WAIT 要等待 2MSL 才会进入 CLOSED 状态。ACK 包到达服务器需要 MSL 时间,服务器重传 FIN 包也需要 MSL 时间,2MSL 是数据包往返的最大时间,如果 2MSL 后还未收到服务器重传的 FIN 包,就说明服务器已经收到了 ACK 包。

Socket中网络子节序

Wikipedia - 字节序

1
2
3
4
5
6
7
8
// host to network short:将short类型从主机字节序转为网络字节序。
uint32_t htonl(unint32_t hostint32);
// host to network long:将long类型从主机字节序转为网络字节序。
uint16_t htons(unint16_t hostint16);
// network to host long:将long类型从主机字节序转为网络字节序。
uint32_t ntohl(unint32_t hostint32);
// network to host short:将short类型从主机字节序转为网络字节序
uint16_t ntohs(unint16_t hostint32);