c语言 - socket编程(一)

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:原始套接字
原始套接字允许对较低层次的协议直接访问,比如IPICMP协议,它常用于检验新的协议实现,或者访问现有服务中配置的新设备;
因为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_INETAF_INET6AF_LOCAL(或称AF_UNIX)、AF_ROUTE等等
    协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位)与端口号(16位)的组合、AF_UNIX决定了要用一个绝对路径名作为地址
  • type:输入参数,socket类型;
    常用的socket类型有:SOCK_STREAMSOCK_DGRAMSOCK_RAWSOCK_PACKETSOCK_SEQPACKET等等
  • protocol:输入参数,指定使用的协议;
    常用的协议有:IPPROTO_TCPIPPTOTO_UDPIPPROTO_SCTPIPPROTO_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:输出参数,返回客户端的地址,可为NULL
  • addrlen:输入参数,指定客户端的地址的长度,可为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,指定对应的选项,一般置为0
  • addr:输出参数,保存该数据的发送方地址
  • 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,指定对应的选项,一般置为0
  • addr:输入参数,指定接收方的地址
  • 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三次握手、四次挥手

tcp三次握手

  1. Client 发送 SYN 包(seq: x),告诉 Server:我要建立连接;Client 进入SYN-SENT状态;
  2. Server 收到 SYN 包后,发送 SYN+ACK 包(seq: y; ack: x+1),告诉它:好的;Server 进入SYN-RCVD状态;
  3. Client 收到 SYN+ACK 包后,发现 ack=x+1,于是进入ESTABLISHED状态,同时发送 ACK 包(seq: x+1; ack: y+1)给 Server;Server 发现 ack=y+1,于是也进入ESTABLISHED状态;

接下来就是互相发送数据、接收数据了……

tcp四次挥手
注意,可以是连接的任意一方主动 close,这里假设 Client 主动关闭连接:

  1. Client 发送 FIN 包,告诉 Server:我已经没有数据要发送了;Client 进入FIN-WAIT-1状态;
  2. Server 收到 FIN 包后,回复 ACK 包,告诉 Client:好的,不过你需要再等会,我可能还有数据要发送;Server 进入CLOSE-WAIT状态;而 Client 收到 ACK 包后,继续等待 Server 做好准备,Client 进入FIN-WAIT-2状态;
  3. Server 准备完毕后,发送 FIN 包,告诉 Client:我也没有什么要发送了,准备关闭连接吧;Server 进入LAST-ACK状态;
  4. 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连接

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回声程序: