socket通信(一)
底层封装了TCP/IP协议簇,开发者只要会用socket即可调用底层协议功能。
socket分为以下两种通信机制:
- stream(流):TCP,基于连接,有序可靠
- datagram(数据包):UDP,不建立连接,不可靠,效率比较高
现在用UDP的场景越来越少了,目前实时音视频聊天会用UDP数据包传输。
下图是socket通信流程:
程序概要
-
socket文件描述符
sockfd = socket(AF_INET,SOCK_STREAM,0)
,socket()函数的返回值其本质是一个文件描述符,是一个整数。 -
绑定ip地址和端口:
// 第2步:把服务端用于通信的地址和端口绑定到socket上。 struct sockaddr_in servaddr; // 服务端地址信息的数据结构。 memset(&servaddr,0,sizeof(servaddr)); servaddr.sin_family = AF_INET; // 协议族,在socket编程中只能是AF_INET。 servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 任意ip地址。 //servaddr.sin_addr.s_addr = inet_addr("192.168.190.134"); // 指定ip地址。 // htons()--"Host to Network Short"使用网络字节顺序,按从高到低的顺序存储(大端) // argv[1]表示后面接的参数 // atoi 字符转为整数 servaddr.sin_port = htons(atoi(argv[1])); // 指定通信端口。 if (bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) != 0 ) { perror("bind"); close(listenfd); return -1; }
client类似server,进行一样的操作,只不过将bind换为connect,连接至目标地址。
-
server要打开监听
if (listen(listenfd,5) != 0 ) { perror("listen"); close(listenfd); return -1; }
-
server接收client的连接
int clientfd; // 客户端的socket。 int socklen=sizeof(struct sockaddr_in); // struct sockaddr_in的大小 struct sockaddr_in clientaddr; // 客户端的地址信息。 // accept后两个参数也可以给0,就看不到client地址 clientfd=accept(listenfd,(struct sockaddr *)&clientaddr,(socklen_t*)&socklen); printf("客户端(%s)已连接。\n",inet_ntoa(clientaddr.sin_addr));
server listen之后,client即可连接,只不过发送的信息都在buffer中,server也不会响应。
accept会返回一个socket,之后server向给client发送消息就是发给这个socket。
-
client发送消息,server接收消息
// 第3步:与服务端通信,发送一个报文后等待回复,然后再发下一个报文。 for (int ii=0;ii<3;ii++) { int iret; memset(buffer,0,sizeof(buffer)); sprintf(buffer,"这是第%d个超级女生,编号%03d。",ii+1,ii+1); if ( (iret=send(sockfd,buffer,strlen(buffer),0))<=0) // 向服务端发送请求报文。 { perror("send"); break; } printf("发送:%s\n",buffer); memset(buffer,0,sizeof(buffer)); if ( (iret=recv(sockfd,buffer,sizeof(buffer),0))<=0) // 接收服务端的回应报文。 { printf("iret=%d\n",iret); break; } printf("接收:%s\n",buffer); }
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
sockfd为已建立好连接的socket(可以是accept返回的,也可以是connect建立的)。
buf为需要发送的数据的内存地址,可以是C语言基本数据类型变量的地址,也可以数组、结构体、字符串,内存中有什么就发送什么。
len需要发送的数据的长度,为buf中有效数据的长度。
flags填0, 其他数值意义不大。
函数返回已发送的字符数。出错时返回-1,错误信息errno被标记。
注意,就算是网络断开,或socket已被对端关闭,send函数不会立即报错,要过几秒才会报错。
如果send函数返回的错误(<=0),表示通信链路已不可用。
// 第5步:与客户端通信,接收客户端发过来的报文后,回复ok。 char buffer[1024]; while (1) { int iret; memset(buffer,0,sizeof(buffer)); if ( (iret=recv(clientfd,buffer,sizeof(buffer),0))<=0) // 接收客户端的请求报文。 { printf("iret=%d\n",iret); break; } printf("接收:%s\n",buffer); strcpy(buffer,"ok"); if ( (iret=send(clientfd,buffer,strlen(buffer),0))<=0) // 向客户端发送响应结果。 { perror("send"); break; } printf("发送:%s\n",buffer); }
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
sockfd为已建立好连接的socket。
buf为用于接收数据的内存地址,可以是C语言基本数据类型变量的地址,也可以数组、结构体、字符串,只要是一块内存就行了。
len需要接收数据的长度,不能超过buf的大小,否则内存溢出。
flags填0, 其他数值意义不大。
函数返回已接收的字符数。出错时返回-1,失败时不会设置errno的值。
如果socket的对端没有发送数据,recv函数就会等待,如果对端发送了数据,函数返回接收到的字符数。出错时返回-1。如果socket被对端关闭,返回值为0。
如果recv函数返回的错误(<=0),表示通信通道已不可用。
-
关闭socket
// 第6步:关闭socket,释放资源。 close(listenfd); close(clientfd);
socket()函数详解
socket函数用于创建一个新的socket,也就是向系统申请一个socket资源。socket函数用户客户端和服务端。
int socket(int domain, int type, int protocol);
domain:协议域,又称协议族(family)。常用的协议族有AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域Socket)、AF_ROUTE等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
type:指定socket类型。常用的socket类型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。流式socket(SOCK_STREAM)是一种面向连接的socket,针对于面向连接的TCP服务应用。数据报式socket(SOCK_DGRAM)是一种无连接的socket,对应于无连接的UDP服务应用。
protocol:指定协议。常用协议有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等,分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。
第一个参数只能填AF_INET,第二个参数只能填SOCK_STREAM(TCP),第三个参数只能填0。
除非系统资料耗尽,socket函数一般不会返回失败。
返回值:成功则返回一个socket,失败返回-1,错误原因存于errno 中。一般会返回3
可以通过man socket
查看编程手册
可打开的socket连接数和linux允许打开的文件数相等,涉及到网络并发压力测试时要修改
sockaddr_in结构体
struct sockaddr_in
{
/*
#define __SOCKADDR_COMMON(sa_prefix) \
sa_family_t sa_prefix##family
*/
__SOCKADDR_COMMON (sin_); //等同于 sa_family_t sin_family 表示地址类型
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
// 为了保持和sockaddr(旧版结构体)一样的长度
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr)
- __SOCKADDR_COMMON_SIZE
- sizeof (in_port_t)
- sizeof (struct in_addr)];
};
struct in_addr
{
// typedef uint32_t in_addr_t;
in_addr_t s_addr;
};
所以声明结构体时,只需要声明三个即可:
- sockaddr_in.sin_family = AF_INET
- sockaddr_in.sin_port
- sockaddr_in.sin_addr.s_addr
最后调用bind函数绑定结构体到socket上即可:
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
参数要求旧的结构体sockaddr ,所以要强转成sockaddr类型。
bind函数问题解决
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
-
监听的端口号有限制,实际就是sockaddr内的端口限制,因为他是用的short int,所以最多表示2的16次方-1:65535。
-
释放的端口要等待两分钟才可以再使用,TIME_WAIT。解决方法:
// socket创建成功后,额外设置一步SO_REUSEADDR int opt = 1; unsigned int len = sizeof(opt); setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, len)
accept函数
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
从已经准备好的连接队列里面获取一个请求,如果队列为空,则阻塞等待,获取了一个连接,队列就会少一个
会返回一个socket文件描述符。
服务端函数调用的流程是:socket->bind->listen->accept->recv/send->close
客户端函数调用的流程是:socket->connect->send/recv->close
其中send/recv可以进行多次交互。
listen负责建立连接,accept负责处理连接。
结合TCP理解
内核为处于listen状态的socket维护两个队列:
- 不完全连接请求队列(SYN_RECV队列)
- 等待accept建立socket的队列(ESTABLISHED队列)
listen函数:
int listen(int sockfd, int backlog);
第二个参数backlog
指等待accept的完全建立的socket的队列长度-2(完成三次握手的队列长度)
而不完全连接请求队列长度使用/proc/sys/net/ipv4/tcp_max_syn_backlog
设置,默认值为128(ubuntu20.04为512)
程序中设置backlog为3,实际可以建立的ESTABLISHED连接为5个;
把backlog换成1,服务器ESTABLISHED连接为3个。