gitee仓库:https://gitee.com/WangZihao64/linux
TCP相关的socket API
listen——将套接字设置为监听状态,然后去监听socket的到来
#include <sys/socket.h>
int listen(int s, int backlog);
参数:
- s:要设置的套接字(称为监听套接字,通过socket创建)
- backlog:连接队列的长度(不建议设置太长,后面的文章会详细介绍这个参数)
返回值: 成功返回0,失败返回-1
accept——接受请求,获取建立好的连接
#include <sys/types.h>
#include <sys/socket.h>
int accept(int s, struct sockaddr *addr, socklen_t *addrlen);
参数:
- s:监听套接字
- addr:输出型参数,获取远端连接的相关信息
- addrlen:输入输出型参数,获取addr的大小长度
返回值: 成功返回一个连接套接字,用来标识远端建立好连接的套接字,失败返回-1
connect——发起请求,请求与服务端建立连接(一般用于客户端向服务端发起请求)
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
- sockfd:套接字,发起连接请求的套接字
- addr:描述自身的相关信息,用来标识自身,需要自己填充,让对端知道是请求方的信息,以便进行响应
- addrlen:描述addr的大小
返回值: 成功返回0,失败返回-1
不知道大家是否对accept
会有疑惑,已经通过socket
创建好了一个套接字,accept又返回了一个套接字,这两个套接字有什么区别吗?UDP只又一个套接字就可以进行通信了,而TCP还需要这么多个,这是为什么?
答案是肯定有的,socket创建的套接字是用来服务端本身进行绑定的。因为UDP是面向数据报,无连接的,所以创建好一个套接字之后直接等待数据到来即可,而TCP是面向连接,需要等待连接的到来,并获取连接,普通的一个套接字是不能够进行连接的监听,这时就需要用的listen来对创建好的套接字进行设置,将其设置为监听状态,这样这个套接字就可以不断监听连接状态,如果连接到来了,就需要通过accept获取连接,获取连接后返回一个值,也是套接字,这个套接字是用来描述每一个建立好的连接,方便维护连接和给对端进行响应,后期都是通过该套接字对客户端进行通信,也就是对客户端进行服务。
所以说,开始创建的套接字是与自身强相关的,用来描述自身,并且需要进行监听,所以我们也会称这个套接字叫做监听套接字,获取到的每一个连接都用一个套接字对其进行唯一性标识,方便维护与服务。
一个通俗的类比,监听套接字好比是一家饭馆拉客的,不断地去店外拉客进店,拉客进店后顾客需要享受服务,这时就是服务员对其进行各种服务,服务员就好比是accept返回的套接字,此时拉客的不需要关心服务员是如何服务顾客的,只需要继续去店外拉客进入店内就餐即可。
基于TCP协议的套接字编程
服务端
TCP服务端的编写分多个版本:多进程、多线程、线程池三个版本,有这么多个版本主要是因为TCP要去服务多个不同的连接,所以单进程目前来看是不现实的。
整体框架
封装一个类,来描述tcp服务端,成员变量包含端口号和监听套接字两个即可,ip像udp服务端一样,绑定INADDR_ANY
,构造函数根据传参初始化port,析构的时候关闭监听套接字即可
class TcpServer
{
public:
TcpServer(uint16_t port)
:_listensock(-1)
,_port(port)
{}
~TcpServer()
{
if (_listen_sock >= 0) close(_listen_sock);
}
private:
int _listensock;
uint16_t _port;
};
}
服务端的初始化
创建套接字
和UDP不同的是,TCP是面向连接的,所以第二个参数和TCP是不同的,填的是SOCK_STREAM
void Init()
{
// 创建套接字
_listensock=socket(AF_INET,SOCK_STREAM,0);
if(_listensock<0)
{
LogMessage(FATAL,"socket create err");
exit(SOCKET_ERR);
}
LogMessage(NORMAL,"socket create success");
}
绑定端口号(和udp一样这里不作介绍)
将套接字设置为监听状态
这里就需要用的listen
这个接口,让套接字处于监听状态,然后可以去监听连接的到来
void Init()
{
if(listen(_listensock,5)==-1)
{
LogMessage(FATAL,"listen err");
exit(LISTEN_ERR);
}
LogMessage(NORMAL,"listen success");
}
循环获取连接
监听套接字通过accept
获取连接,一次获取连接失败不要直接将服务端关闭,而是重新去获取连接就好,因为获取一个连接失败而直接关闭服务端,带来的损失是很大的,所以只需要重新获取连接即可,返回的用于通信套接字记录下来,进行通信,然后可以用多种方式为各种连接连接提供服务
void start()
{
for(;;) {
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(_listensock, (struct sockaddr *) &peer, &len);
if (sock < 0) {
LogMessage(ERROR, "accept err");
continue;
}
LogMessage(NORMAL, "accept success %d ", sock);
}
}
客户端
很多地方和服务器差不多,不再介绍,这里只介绍重点
客户端启动
发起连接请求
使用connect
函数,想服务端发起连接请求,注意,调用这个函数之前,需要先填充好服务端的信息,有协议家族、端口号和IP,请求连接失败直接退出进程,重新启动进程即可,连接成功之后就可以像服务端发起各自的服务请求(后面介绍),代码如下:
void start()
{
struct sockaddr_in send;
bzero(&send, sizeof(send));
send.sin_family = AF_INET;
send.sin_port = htons(_port);
send.sin_addr.s_addr = inet_addr(_ip.c_str());
if (connect(_sockfd, (struct sockaddr *)&send, sizeof(send)) == -1)
{
LogMessage(FATAL, "connect err");
exit(CONNECT_ERR);
}
else
{
}
}
发起服务请求
请求很简单,只需要让用户输入字符串请求,然后将请求通过write
(send也可以)发送过去,然后创建一个缓冲区,通过read
(recv也可以)读取服务端的响应,这里需要着重介绍一下read
的返回值
read的返回值:
- 大于0:实际读取的字节数
- 等于0:读到了文件末尾,说明对端关闭,用在服务端就是客户端关闭,用在客户端就是服务端关闭了,客户端可以直接退出
- 小于0:说明读取失败
else
{
while (1)
{
// 发送消息
cout << "Enter# ";
string message;
getline(cin,message);
write(_sockfd, message.c_str(), message.size());
char buffer[1024];
int n=read(_sockfd,buffer,sizeof(buffer)-1);
if(n>0)
{
buffer[n]=0;
cout << "Server回显# " << buffer << endl;
}
else
{
break;
}
}
}