c语言 - socket编程(一),源IP地址和目的IP地址以及源端口号和目的端口号的组合称为套接字,其用于标识客户端请求的服务器和服务;
它是网络通信过程中端点的抽象表示,包含进行网络通信必需的五种信息:连接使用的协议
,本地主机的IP地址
,本地进程的协议端口
,远地主机的IP地址
,远地进程的协议端口
socket套接字
网络主机之间的应用程序如何进行通信?
网络层的ip地址
可以唯一标识网络中的主机;
而传输层的协议+端口
可以唯一标识主机中的应用程序(进程);
这样利用三元组(ip地址 + 协议 + 端口)
就可以标识网络的进程
了,网络中的进程通信就可以利用这个标志与其它进程进行交互
什么是socket套接字?
上面我们已经知道网络中的进程是通过socket来通信的,那什么是socket呢?
socket起源于Unix,而Unix/Linux基本哲学之一就是一切皆文件
,都可以用打开open –> 读写read/write –> 关闭close
模式来操作;
socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭);
SOCK_STREAM
:流套接字
流套接字用于提供面向连接
、可靠的数据传输
服务;该服务将保证数据能够实现无差错、无重复发送,并按顺序接收;
流套接字之所以能够实现可靠的数据服务,原因在于其使用了传输控制协议,即TCP协议
SOCK_DGRAM
:数据报套接字
数据报套接字提供了一种无连接
的服务;该服务并不能保证数据传输的可靠性
,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据;
数据报套接字使用UDP协议
进行数据的传输;由于数据报套接字不能保证数据传输的可靠性,对于有可能出现的数据丢失情况,需要在程序中做相应的处理
SOCK_RAW
:原始套接字
原始套接字允许对较低层次的协议直接访问
,比如IP
、ICMP
协议,它常用于检验新的协议实现,或者访问现有服务中配置的新设备;
因为RAW SOCKET可以自如地控制网络底层的传输机制,所以可以应用原始套接字来操纵网络层和传输层应用;
比如,我们可以通过RAW SOCKET来接收发向本机的ICMP、IGMP协议包,或者接收TCP/IP栈不能够处理的IP包,也可以用来发送一些自定包头或自定协议的IP包;
原始套接字与标准套接字(标准套接字指的是前面介绍的流套接字和数据包套接字)的区别在于:
原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据;因此,如果要访问其他协议发送数据必须使用原始套接字
socket基本操作
int socket(int domain, int type, int protocol);
:创建一个socket文件描述符fd
domain
:输入参数,协议域、地址域或协议族
;
常用的协议族有:AF_INET
、AF_INET6
、AF_LOCAL
(或称AF_UNIX
)、AF_ROUTE
等等
协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位)与端口号(16位)的组合、AF_UNIX决定了要用一个绝对路径名作为地址type
:输入参数,socket类型;
常用的socket类型有:SOCK_STREAM
、SOCK_DGRAM
、SOCK_RAW
、SOCK_PACKET
、SOCK_SEQPACKET
等等protocol
:输入参数,指定使用的协议;
常用的协议有:IPPROTO_TCP
、IPPTOTO_UDP
、IPPROTO_SCTP
、IPPROTO_TIPC
等等
如果该参数为0,则让系统自动选择合适的协议,一般我们也这么操作- 返回值:成功返回一个非负整数的fd,失败则返回-1,并设置errno
当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族空间中,但没有一个具体的地址;
如果想要给它赋值一个地址,就必须调用bind()
函数,否则就当调用connect()
、listen()
时系统会自动随机分配一个端口
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
:绑定一个固定的socket地址
sockfd
:输入参数,socket文件描述符,即socket()的返回值addr
:输入参数,指向sock地址的指针addrlen
:输入参数,sock地址的长度- 返回值:成功返回0,失败返回-1,并设置errno
int listen(int sockfd, int backlog);
:监听socket套接字
sockfd
:输入参数,被监听的套接字backlog
:输入参数,最大等待队列数量,宏SOMAXCONN
为系统设定的最大值,可通过/etc/sysctl.conf
修改- 返回值:成功返回0,失败返回-1,并设置errno
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
:连接socket套接字
sockfd
:输入参数,客户端的套接字addr
:输入参数,服务器的地址addrlen
:输入参数,服务器的地址的长度- 返回值:成功返回0,失败返回-1,并设置errno
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
:接受socket连接请求
sockfd
:输入参数,被监听的套接字addr
:输出参数,返回客户端的地址,可为NULLaddrlen
:输入参数,指定客户端的地址的长度,可为NULL- 返回值:成功返回已连接的新套接字描述符connfd,失败返回-1,并设置errno
int recv(int sockfd, void *buf, int len, int flags);
:从socket接收数据
sockfd
:输入参数,从这个socket接收数据buf
:输出参数,用来保存接收到的数据len
:输入参数,指定buf的长度,表示最多接收这么多个字节的数据flags
:输入参数,flags,指定对应的选项,一般置为0- 返回值:成功返回接收到的数据大小,返回0表示对方不再发送数据(可以理解为关闭了连接),出错返回-1,并设置errno
int send(int sockfd, void *buf, int len, int flags);
:向socket发送数据
sockfd
:输入参数,向这个socket发送数据buf
:输入参数,发送buf指向的数据len
:输入参数,指定buf的长度,指定要发送的数据大小flags
:输入参数,flags,指定对应的选项,一般置为0- 返回值:成功返回已发送的数据大小,失败则返回-1,并设置errno
int recvfrom(int sockfd, void *buf, int len, int flags, struct sockaddr *addr, socklen_t *addrlen);
:从udp socket接收数据
sockfd
:输入参数,从该socket接收数据buf
:输出参数,将接收的数据存放在buf上len
:输入参数,指定buf的长度flags
:输入参数,flags,指定对应的选项,一般置为0addr
:输出参数,保存该数据的发送方地址addrlen
:输入参数,指定发送方地址的长度- 返回值:成功返回接收到的数据大小,失败则返回-1,并设置errno
int sendto(int sockfd, void *buf, int len, int flags, struct sockaddr *addr, socklen_t addrlen);
:向udp socket发送数据
sockfd
:输入参数,向该socket发送数据buf
:输入参数,发送buf指向的数据len
:输入参数,指定buf的长度flags
:输入参数,flags,指定对应的选项,一般置为0addr
:输入参数,指定接收方的地址addrlen
:输入参数,指定接收方的地址的长度- 返回值:成功返回发送的数据大小,失败则返回-1,并设置errno
最后一个参数flags
:
MSG_WAITALL
:用于recv,尽可能等待所有数据MSG_DONTWAIT
:用于recv、send,仅本次操作不阻塞MSG_DONTROUTE
:用于send,绕过路由表查找MSG_OOB
:用于recv、send,发送或接收带外数据MSG_PEEK
:用于recv,窥看外来数据,不将其从socket缓冲区中删除
int shutdown(int sockfd, int howto);
:关闭tcp连接
sockfd
:输入参数,即将关闭连接的套接字howto
:输入参数,定义如何关闭:SHUT_RD
值为0,关闭读、SHUT_WR
值为1,关闭写、SHUT_RDWR
值为2,关闭读写- 返回值:成功返回0,失败返回-1,并设置errno
int close(int fd);
:关闭socket
fd
:输入参数,要关闭的文件描述符fd- 返回值:成功返回0,失败返回-1,并设置errno
shutdown和close的区别shutdown()
函数用于tcp连接的socket套接字,对udp无效
用于更精细的控制流的读和写,可以只关闭读,也可以只关闭写,或者读写都关闭,但是关闭后,该sockfd依旧是有效的,仍需调用close进行关闭
而close()
函数用于关闭对应的fd文件描述符,socket也是特殊的文件,注意,close只是将对应的sockfd的引用计数减1,当引用计数减到0时,系统才关闭该sockfd
还有一点:在多进程程序中,使用shutdown会导致共享进程的连接也被关闭,读写出现错误,而close不会影响共享进程的socket
tcp三次握手、四次挥手
- Client 发送 SYN 包(seq: x),告诉 Server:我要建立连接;Client 进入
SYN-SENT
状态; - Server 收到 SYN 包后,发送 SYN+ACK 包(seq: y; ack: x+1),告诉它:好的;Server 进入
SYN-RCVD
状态; - Client 收到 SYN+ACK 包后,发现 ack=x+1,于是进入
ESTABLISHED
状态,同时发送 ACK 包(seq: x+1; ack: y+1)给 Server;Server 发现 ack=y+1,于是也进入ESTABLISHED
状态;
接下来就是互相发送数据、接收数据了……
注意,可以是连接的任意一方主动 close,这里假设 Client 主动关闭连接:
- Client 发送 FIN 包,告诉 Server:我已经没有数据要发送了;Client 进入
FIN-WAIT-1
状态; - Server 收到 FIN 包后,回复 ACK 包,告诉 Client:好的,不过你需要再等会,我可能还有数据要发送;Server 进入
CLOSE-WAIT
状态;而 Client 收到 ACK 包后,继续等待 Server 做好准备,Client 进入FIN-WAIT-2
状态; - Server 准备完毕后,发送 FIN 包,告诉 Client:我也没有什么要发送了,准备关闭连接吧;Server 进入
LAST-ACK
状态; - Client 收到 FIN 包后,知道 Server 准备完毕了,于是给它回复 ACK 包,告诉它我知道了,于是进入
TIME-WAIT
状态;而 Server 收到 ACK 包后,即进入CLOSED
状态;Client 等待 2MSL 时间后,没有再次收到 Server 的 FIN 包,于是确认 Server 收到了 ACK 包并且已关闭,于是 Client 也进入CLOSED
状态;
MSL
即报文最大生存时间
,RFC793 中规定 MSL 为 2 分钟,实际应用中常用的是 30 秒、1 分钟、2 分钟等;可以修改/etc/sysctl.conf
内核参数,来缩短TIME_WAIT
的时间,避免不必要的资源浪费。
最后附上tcp从建立连接到传输数据到释放连接的过程图:
tcp_socket实例
实现一个echo回声的服务端/客户端,即client发送一条消息给server,server就把该消息原样返回给client:
tcp_echo_server.c
tcp_echo_client.c
测试echo回声程序:
udp_socket实例
我们也实现一个echo回声:
udp_echo_server.c
udp_echo_client.c
测试echo回声程序: