基于TCP网络编程API
什么是 socket?
socket
起源于UNIX,而 UNIX/Linux 基本哲学之一就是”一切皆文件”,都可以用打开(open) -> 读写(write/read) -> 关闭(close)
模式来操作。socket
其实就是该模式的一个实现,socket
即是一种特殊的文件,一些socket
函数就是对其进行的操作(读/写、 打开、关闭)。
socket 的基本操作
socket
是open-write/read-close
模式的一种实现,那么socket
就提供了这些操作对应的函数接口,本文将以TCP
协议通信socket
为例,讲解相关的函数接口
TCP三次握手
TCP交互流程
socket 函数
socket
函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述符,而socket()
用于创建一个socket
描述符(socket description
),它唯一标识一个socket
。这个socket
描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它进行一些读写操作1
int socket(int domain, int type, int protocol);
参数简述
domain
:协议域,又称为协议族(family
)。常用的协议族有:AF_INEF
、AF_INET6
、AF_LOCAL
等。协议族决定了socket
的地址类型,在通信种必须采用对应的地址,如AF_INEF
决定了要用ipv4地址(322)位与端口号(16位)type
:指定socket类型。常用的socket类型有:SOCK_STREAM
、SOCK_DGRAM
等等。其中,SOCK_STREAM
表示提供面向连接的稳定数据传输,即 TCP 协议。SOCK_DGRAM
表示使用不连续、不可靠的数据包连接protocol
:指定协议。常用的协议有:IPPROTO_TCP
,IPPTOTO_UDP
,IPPROTO _ SCTP
、IPPROTO_TIPC
等,它们分别对应 TCP 传输协议、 UDP 传输协议、 STCP 传输协议、 TIPC 传输协议
bind 函数
bind()
函数把一个地址族中的特定地址赋给 socket
1 | int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen); |
参数简述
sockfd
:即socket
描述字,它是通过socket()
函数创建来唯一标识一个socket的。bind()
函数就是将给这个描述字绑定一个名字addr
:一个const struct sockaddr*
指针,指向要绑定给sockfd
的协议地址。 这个地 结构根据地址创建socket
时的地址协议族的不同而不同addrlen
:对应的是地址的长度
listen 和 connect 函数
如果作为一个服务器,在调用socket()
、bind()
之后就会调用listen()
来监听这个socket
,如果客户端这时调用connect()
发出连接请求,服务器端就会接收到这个请求。1
int listen(int sockfd, int backlog);
listen
函数的第一个参数即为要监听的socket
描述字,第二个参数为相应socket
可以排队的最大连接个数。socket()
函数创建的 socket
默认是一个主动类型的, listen
函数将 socket
变为被动类型的,等待客户的连接请求。
1 | int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); |
connect
函数的第一个参数即为客户端的socket
描述字,第二参数为服务器的socket
地址,第三个参数为socket
地址的长度。客户端通过调用connect
函数来建立与TCP服务器的连接
accept 函数
TCP 服务器端依次调用 socket()
、bind()
、listen()
之后,就会监听指定的socket
地址了。TCP 客户端依次调用 socket()
、connect()
之后就会向 TCP 服务器发送了一个连接请求。 TCP 服务器监听到这个请求之后,就会调用accept()
取接收请求,这样连接就建立好了。 之后就可以开始网络 I/O 操作了,即类同于普通文件的读写 I/O 操作。
1 | int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); |
accept
函数的第一个参数为服务器的 socket
描述字;第二个参数为指向 struct sockaddr*
的指针,用于返回客户端的协议地址;第三个参数为协议地址的长度。如果 accpet
成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的 TCP 连接
注意:accept
的第一个参数为服务器的 socket
描述字,是服务器开始调用 socket()
函数生成的,称为监听socket
描述字;而 accept
函数返回的是己连接的 socket
描述字。 一个服务器通常仅仅只创建一个监听 socket
描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户创建了一个已连接 socket
描述字,当服务器完成了对某个客户的服务,相应的已连接 socket
描述字就被关闭
read 和 write 函数
至此服务器与客户已经建立好连接了,可以调用网络 I/O 进行读写操作了,即实现了网络中不同进程之间的通信。网络 I/O 操作有下面几组
read() / write()
recv() / send()
readv() / writev()
recvmsg() / sendmsg()
recvfrom() / sendto()
比较常用的是read
和write
1
ssize_t read(int fd, void *buf, size_t count);
read()
函数是负责从fd中读取内容。当读取成功时,read()
返回实际所读的字节数,如果返回的值是 0 表示已经读到文件的结束了,小于 0 表示出现了错误。如果错误为 EINTR
说明读是由中断引起的,如果是 ECONNREST
表示网络连接出了问题。
socket
描述字fd
- 缓冲区
buf
- 缓冲区长度
count
。
1 | ssize_t write(int fd, void *buf, size_t count); |
write()
函数将buf中的 nbytes 字节内容写入文件描述符fd成功时返回写的字节数。失败时返回-1,并设置 errno
变量。在网络程序中,当我们向套接字文件描述符写时有两种可能: 1.write
的返回值大于0,表示写了部分或者是全部的数据; 2.返回的值小于 0,此时出现了错误。实际中要根据错误类型来处理。如果错误为 EINTR
表示在写的时候出现了中断错误。如果为 EPIPE
表示网络连接出现了问题(对方已经关闭了连接)
- fd 表示 socket 描述字
- buf表示缓冲区
- count 表示缓冲区长度
close 函数
在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的 socket
描述字
1 |
|
close
一个 TCP socket
的默认行为时,会把该 socket
标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为 read
或 write
的第一个参数
实现简单的TCP-Server
该实现主要是了解一些相关API的调用,起主要参考的TCP交互流程即可 - 项目 - TCP-Server
网络字节序与主机序
主机序
- 不同的CPU
有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,称为主机序。最常见的就是大端和小端Little Endian
- 小端: 把地址低位存储值的低位,地址高位存储值的高位Big Endian
- 大端: 把地址低位存储值的高位,地址高位存储值的低位
网络字节序
- 4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP
首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序- 网络协议也都是采用
Big Endian
的方式来传输数据的。 所以有时也会把Big Endian
方式称之为网络字节序。 当两台采用不同字节序的主机通信时,在发送数据之前都必须经过字节序的转换成网络字节序后再进行传输