socket也就是套接字,套接字对于程序猿来说是一套网络通信的接口,我们使用这套接口就可以完成网络通信。网络通信的主体主要可以分成2部分:客户端和服务端。而这里主要关注服务器端。
而之前也说了,我们这个服务器是基于TCP通信的。
1.TCP通信流程
TCP是一个面向连接的,安全可靠的,流式传输协议,这个协议是在传输层。
2.服务器端的通信流程
1.创建用于监听的套接字(文件描述符)
// 创建一个套接字, 函数原型
int socket(int domain, int type, int protocol);
//使用
int fd=socket(AF_INET,SOCK_STREAM,0);
使用这个函数需要包含头文件<sys/socket.h>,若是已包含头文件<arpa/inet.h>,则前者不需要再包含了。
参数说明:
- domain:使用的地址族协议(AF_INET表示使用IPv4格式的ip,AF_INET6表示使用IPv6格式的ip)
- type:数据的传输协议(SOCK_STREAM表示使用流式的传输协议(TCP),SOCK_DGREAM表示使用报文式的传输协议(UDP))
- protocol:一般写0就行,表示使用默认的协议
返回值:
- 成功放回可用于套接字通信的文件描述符,失败返回-1.
2.将文件描述符和本地的ip地址和端口进行绑定
//函数原型
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//使用
//设置服务器的地址族,ip和端口
strcut sockaddr_in addr;
memset(&addr,0,sizeof(addr));
addr.sin_family=AF_IENT;
addr.sin_port=htons(10000);
addr.sin_addr.s_addr=htonl(INADDR_ANY);
int ret=bind(fd,(struct sockaddr*)&addr,sizeof(addr));
参数:
- sockfd:服务器监听的文件描述符,是通过前面的socket()函数调用得到的返回值
- addr:这是个传入参数,要讲绑定的ip和端口信息初始化到这个结构体中
- addrlen:参数addr指向的内存大小,sizeof(struct sockaddr);
返回值:
- 成功返回0,失败返回-1.
3.给监听的套接字设置监听
//函数原型
int listen(int sockfd,int backlog);
//使用
int ret=listen(fd,128);
参数:
- sockfd:文件描述符,也是通过socket()得到,但需要在使用bind()之后才可以给listen()使用
- backlog:同时能处理的最大连接要求数量。
返回值:
- 成功返回0,失败返回-1。
4.等待接受客户端的连接请求
// 函数原型
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
//使用
struct sockaddr* caddr;
memset(&caddr,0,sizeof(caddr));
socklen_t len=sizeof(caddr);
int cfd=accept(fd,(struct sockaddr*)&caddr,&len);
参数:
- sockfd:文件描述符,也是通过socket()得到。
- addr:传出参数,内部是存储了简历连接的客户端的地址信息
- addrlen:传出参数,存储addr指向的内存大小
返回值:
- 成功得到一个文件描述符,用来和建立连接的这个客户端进行通信,失败返回-1。
注意:这个函数是一个阻塞函数,当没有新的客户端连接请求的时候,该函数会阻塞等待;当检测到有新的客户端连接请求时,阻塞解除,新连接就建立了,得到的返回值也是一个文件描述符,基于这个文件描述符就可以和客户端通信了。
5.与客户端进行通信
// 接收数据
ssize_t read(int sockfd, void *buf, size_t size);
ssize_t recv(int sockfd, void *buf, size_t size, int flags);
//使用
char buf[1024]={0};
int len=read(cfd,buf,sizeof(buf));
参数:
- sockfd: 用于通信的文件描述符,即accept () 函数的返回值
- buf: 指向一块有效内存,用于存储接收的数据
- size: 参数 buf 指向的内存的容量
- flags: 特殊的属性,一般指定为 0
返回值:有三种情况
- 大于 0:实际接收的字节数
- 等于 0:对方断开了连接
- -1:接收数据失败了
// 发送数据的函数 函数原型
ssize_t write(int fd, const void *buf, size_t len);
ssize_t send(int fd, const void *buf, size_t len, int flags);
//使用
char buf[1024]="23234234";
int size=write(cfd,buf,strlen(buf));
参数:
- fd: 通信的文件描述符,accept () 函数的返回值
- buf: 传入参数,要发送的字符串
- len: 要发送的字符串的长度
- flags: 特殊的属性,一般不使用,指定为 0
返回值:
- 大于 0:实际发送的字节数,和参数 len 是相等的
- -1:发送数据失败了
剩下的就还有客户端使用的函数
//函数原型
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//客户端使用
// 连接服务器
struct sockaddr_in addr;
memset(&addr,0,sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(10000);
inet_pton(AF_INET, "1.1.1.1", &addr.sin_addr.s_addr);//第二个参数是服务端的ip地址
int ret=connect(fd,(struct sockaddr*)&addr,sizeof(addr));
按照流程图可以搭建出个简单的echo服务器了。代码放在GitHub上,code/server_v1内。
需要先按照自己电脑的linux环境的ip地址修改client.cpp的ip,接着使用make命令编译后,打开两个终端,一个先输入./Server运行服务端,另一终端再输入./client运行客户端,这样就可以进行通信了,这只是简单的回显(客户端发送什么,服务端就给客户端返回原来的信息)。
这里演示的代码为了简便,代码块内的使用是没有写出错处理的代码,这编程过程中是需要写的。在完整源代码内是写了出错处理的,这是为了可以快速定位到程序出错的位置和发生的错误。
这一节就用socket编程简单实现了个echo服务器。
完整源代码:https://github.com/liwook/CPPServer/tree/main/code/server_v1