几大函数
socket
int socket(int family, int type, int protocol);
//成功则返回非0描述符,失败则返回-1
family:表示协议族,IPV4表示AF_INET
type:表示套接字类型,TCP表示SOCK_STREAM,UDP为SOCK_DGRAM
protocol默认为0
bind
bind函数将一个本地协议地址赋予一个套接字
int bind(int sockfd, const struct sockaddr* myaddr, socklen_t addrlen);
//成功返回0,失败返回-1
- bind可以绑定的端口号为0,此时内核将在bind被调用时选择一个临时端口
- bind可以绑定的IP为通配IP INADDR_ANY,当客户端发起连接后,内核根据客户端的目的IP来指定该连接的本地IP
- bind常见的返回错误为EADDRINUSE,该错误发生在当该端口号已经被绑定到了某个套接字时,如果再次绑定该端口号将出现该错误,(可以通过SO_REUSEADDR和SO_REUSEPORT来修改套接字选项)
listen
int listen(int sockfd, int backlog);
//成功返回0,失败返回-1
listen的关键还是两个队列,即未完成队列和已完成队列,详见TCP/IP节
accept
int accept(int sockfd, struct sockaddr* clienaddr, socklen_t* addrlen);
accept表示从已完成连接队列中取出一个连接,如果成功返回则返回一个由内核生成的全新描述符,该描述符用于和客户端建立连接。注:在服务器中包含两种描述符,即监听描述符和已连接描述符,其中已连接描述符表示和客户端所建立的连接,该描述符的地址和端口还是监听描述符所绑定的地址和端口号
在阻塞模式下,如果已完成连接队列为空,则accept会阻塞
accept的返回值错误情况:
- EINTR:当accept阻塞时被信号中断,则返回此错误
- EAGAIN/EWOULDBLOCK:非阻塞模式下,当已完成队列为空,返回该错误
- 当三次握手完成,在调用accept之前,如果客户端发送了一个RST,此时调用accept,那么不同的实现效果不同,Berkeley的实现是在内核中中止该连接,而服务器进程看不到,因此会阻塞,但是SVR4等实现则是返回一个错误ECONNABORTED,此时只需要重新调用accept即可,所以这也是为什么要IO多路复用搭配非阻塞模式的原因,否则此时accept将一直阻塞,但是在epoll的ET下,如果用非阻塞模式则要循环accept
connect
int connect(int sockfd, struct sockaddr* serv_addr, int addrlen);
该函数用于和服务器建立连接,即用于发起三次握手,connect之前,不必给sockfd,bind端口和源IP,内核会在调用connect后自动为其分配和绑定
connect如果采用阻塞模式,则将在发起连接请求后等待对方响应SYN,如果一段时间内未收到,则重发连接请求报文,若等待了75s(75s到几分钟)后还没有收到SYN则放弃,并返回错误
connect的各种错误返回:
- ETIMEDOUT:连接超时,即75s内没有收到对方的SYN响应
- ECONNREFUSED:若服务器对客户端的响应是RST,则返回该错误,出现这种错误的可能是服务器在指定的端口号没有进程在监听
- EHOSTUNREACH/ENETUNREACH,连接请求报文目的地址不可达时(收到IMCP错误),则将同1重发,75s超时后返回该错误。
- EBADF:参数sockfd不是合法的socket,即可能是关闭了sockfd,然后又用sockfd去处理
- EFAULT:参数serv_addr指针指向无法存取的内存空间
- ENOTSOCK:sockfd是一文件描述词,不是socket
- EISCONN:参数sockfd的socket已经连接,即该socket已经建立了连接
- EAFNOSUPPORT:sockaddr结构的sa_family不正确
- EINPROGRESS:当connect为非阻塞时,返回此错误,表示连接请求正在进行还没有完成
- EADDRINUSE:本地地址处于使用状态
- EALREADY:套接字为非阻塞,并且之前有连接请求还没有完成
- EINTR:connect如果被信号中断将返回该错误,即慢系统调用被信号中断,当connect被EINTR中断时,不能再次重启connect,这是因为已经发起了连接请求,如果再次调用将返回错误,此时必须等待select/epoll将连接请求完成
connect需要额外的说一下,一般而言connect用非阻塞模式去连接服务器,这是因为如果用connect去连接,那么connect的超时时间较长(75s,甚至更长),而有的程序希望在短时间内结束等待,所以可以将connect设置为非阻塞,当连接失败时用select/epoll去等待很短时间,如果还没有连接,则放弃连接
非阻塞模式下的connect流程如下:
- 调用connect,如果返回0,则表示成功建立连接(一般如果客户端和服务器在同一台主机上可能会出现这种情况),正常情况是返回-1,错误码是EINPROGRESS
- 将该sock注册到select/epoll中,利用select/epoll去检测连接是否成功建立,如果成功建立则返回连接可写(连接建立后,写缓冲区空闲,故可写),如果连接建立失败,则返回可写可读(socket出现错误,连接可读可写)
- 因此需要进一步判断连接是否建立成功,常见的方法有两种:
- 调用getsockopt来检测描述符是否出现错误,如果没有错误则获取的error为0,连接成功建立,如果有错误则可以得到相应的错误值,表示连接建立失败
- 第二种方法是此时再调用connect,如果errno是EISCONN则连接已经建立,否则任务连接建立失败
https://blog.csdn.net/nphyez/article/details/10268723
https://blog.csdn.net/u012377333/article/details/45391921
read/write
size_t read(int socket, void* buf, size_t length);
size_t write(int socket, const void* buf, size_t length);
-
read
- read是从TCP内核缓冲区中读取数据,将读取到的数据放入应用层缓冲区中
- 其返回成功读出的数据字节数(可能是将缓冲区中的剩余数据读完了,也可能是读取了一部分被信号打断导致返回)
- 读取失败则返回-1,并设置errno
- read返回0可能有两种情况,其一是read的length为0,其二是read读取到了文件结束符
- read的常见错误码
- EAGAIN/EWOULDBLOCK:fd设定为非阻塞,且要被阻塞时立即返回-1,设定相应的errno
- EINTR:慢系统调用常见的错误,当系统调用函数阻塞时被其他信号打断需要执行信号函数,在信号函数处理完毕后则返回此错误,在read中常见的包括阻塞时被打断则返回该错误,如果是在读取时被打断则返回实际读取的字节数。
- 关于该错误,发生该错误时可以考虑重启该系统调用即可(即忽略该错误)
- ECONNREST,当read从一个收到RST包的socket上读取数据时,将会返回该错误
- ETIMEDOUT,当对端主机崩溃,则超时后, 套接字将被标记为此错误,调用read将出现此错误
- EHOSTUNREACH,当对端主机不可达,出现此错误
-
write
-
write即将数据从应用层缓冲区拷贝到内核缓冲区,需要注意,write成功执行只能表明数据成功拷贝进入了缓冲区,但是不能保证数据已经成功发送了。
-
write返回值如下:
- 等于length
- 小于length,在阻塞模式下,write如果没有完成需要写length字节,则会阻塞直到写完为止,但是在非阻塞模式下,则是返回实际写入的字节数,也可能是写入过程中被中断信号打断
- 返回0,write返回0,一般是因为length为0
-
write返回错误的情况:
- EAGAIN/EWOULDBLOCK 非阻塞模式下,如果内核缓冲区没有空间则会返回-1
- EINTR,如果write没有写入一个字节就阻塞了,那么将返回-1,否则返回实际写入的字节数
-
EPIPE:fd是一个pipe或者socket时,收到SIGPIPE信号时会返回该错误
- ETIMEDOUT,当对端主机崩溃,则超时后, 套接字将被标记为此错误,调用read将出现此错误
- EHOSTUNREACH,当对端主机不可达,出现此错误
-
-
read和fread的区别/write和fwrite的区别
-
fread为库函数,而read为系统函数,一般而言fread效率高,因为fread有缓存,且缓存为系统自动分配,如果要读取8K的数据,但是只分配了2K的缓存,则read需要读取4次磁盘,发生4次系统调用,而fread则会自动分配缓存,先将8K一次性从磁盘中读取,只需要一次系统调用,等再读取数据时直接从缓冲区中读取即可
-
fwrite也是一样,write如果需要写入10个字节到内核,则会直接写入到内核缓冲区,而fwrtie则是先写入到应用缓冲区,等写满后再调用fflush将缓冲区中的数据写入内核,这样减少了系统调用
-
https://blog.csdn.net/q2519008/article/details/84308726
-
fread(void* buf, size_t size,1, FILE* fp); fwrite(void* buf, 1, size_t size, FILE* fp);
-
https://blog.csdn.net/songchuwang1868/article/details/90665865
https://blog.csdn.net/dangzhangjing97/article/details/79619894
close/shutdown
close: close在关闭fd时,只是将fd的引用计数减1,如果fd的引用计数为0,那么则会关闭此套接字(关闭读写缓冲区),注意close会关闭套接字的读和写,同时触发连接关闭操作,此时向对端发送FIN包,对端回送ACK,注意TCP允许半关闭存在,所以此时对端还可以向本端发送数据,但是close却关闭了本端的读写,所以此时如果本端收到了对端的数据,将会回复对方RST包,即如果一方调用了close,但是实际上最好还是将两条连接都关闭
- 当client调用close关闭连接时,如果client存在收到的远端的,没有处理的消息,这时close要丢弃这些消息。但丢弃这些数据意味着server端以为发出的消息已经被本机成功处理(ACK确认过了),但是实际上并未处理,此时不会使用正常的四次握手关闭,而是会向server发送一个RST非正常复位关闭连接,所以这样要求在编写代码时要保证在关闭连接前已经接收、处理了连接上的消息。(如果某端连续两次写入,在第一次写入后收到了RST,则第二次写入一定会引起SIGPIPE错误)如果某端收到了RST,那么RST不会立即交给应用层,等下一次在调用write函数写入时,会导致SIGPIPE信号,该信号默认终止进程,但是最好忽略该信号,此时的write会返回EPIPE错误
- 当没有未处理的消息时,client在调用close后会首先确保要发送的数据都尽量发出,然后发出FIN包,此时client不会再通过连接接收和发送数据。此时如果server调用read会返回0,跳出循环,也执行close操作从而关闭server端的连接。但是此时server还是可以write的,write只是负责把数据交给TCP的发送缓冲区就可以成功的返回,所以并不会报错,当TCP连接将数据再次发送给client时,由于之前client已经关闭了连接,所以client收到数据后会应答一个RST段,表示其不会再收到数据,连接重置。Server端收到RST后并不会立即通知应用层,只把这个状态保存再TCP协议层。若server再次调用write发送给client,由于其已经处于RST状态,因此不会将数据发出,而是发一个信号给SIGPIPE给应用层,从而终止程序(该信号的默认处理是终止程序)。
shutdown
int shutdown(int sockfd, int howto);
/*
howto的取值包括
1.SHUT_RD
2.SHUT_WR
3.SHUT_RDWR
*/
shutdown则不会理会引用计数的问题,而是直接破坏TCP连接,且会等到将缓冲区中的数据发送完毕后,直接发送FIN,但是shutdown不会释放掉套接字,即调用了shutdown还是需要close来释放套接字
- Shutdown关闭读时,则client不再读取数据,但是若server给client发送数据,则client根据操作系统的不同采取不同的措施,UNIX则回复ack,并扔掉该数据,linux回复ack并缓冲区下来,win会回复RST(注意关闭读,不会触发TCP连接的关闭操作)
- Shutdown关闭写时,client不再向连接写,则会触发TCP连接的关闭操作,client向server发送FIN包,但是server仍然可以发送数据给client,且client可以接收数据。(这个很重要,即shutdown只是关闭写,但是另一方还是可以继续给本端发送数据的,本端还是可以继续收到数据的,但是关闭了写那么FIN包就已经发送过去了,如果对端调用了read就会读到FIN)
- Shutdown关闭读和写则类比close,但是注意,shutdown始终不会关闭socket id本身即此fd最终还是需要close来关闭。(在muduo中是通过RAII的方式最后来close fd,但是连接关闭是通过shutdown的方式)
最后我谈谈自己对close和shutdown的理解,这两个函数其实对应了四次挥手的两种不同做法,shutdown可以只关闭写,同时触发连接关闭,这是客户端是可以继续接收服务器的数据的,等到服务器发送数据完毕,服务器也关闭了连接,此时四次挥手结束,但是close的关闭就不是那么优雅了,其直接关闭了客户端的读和写,然后还触发了连接关闭,一旦对端再给自己发送数据,那么就触发RST包,简单粗暴的让对端关闭连接
https://blog.csdn.net/xyyaiguozhe/article/details/30252559
https://blog.csdn.net/qq_33113661/article/details/88428604