1. 套接字的初步认识
套接字是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。
套接字是通信的基石,是支持TCP/IP协议的路通信的基本操作单元。
2.套接字的主要类型
流套接字(SOCK_STREAM)
流套接字用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复送,并按顺序接收。
流套接字使用tcp协议。
数据报套接字(SOCK_DGRAM)
数据报套接字提供一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。
数据报套接字使用udp协议。
原始套接字(SOCK_RAW)
原始套接字与标准套接字(标准套接字指的是前面介绍的流套接字和数据报套接字)的区别在于:原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送的数据必须使用原始套接字。
3.字节序转换接口
//主机IP转换为网络IP
uint32_t htonl(uint32_t hostlong);
//主机端口号转换为网络端口号
uint16_t htons(uint16_t hostshort);
//网络IP转换为主机IP
uint32_t ntohl(uint32_t netlong);
//网络端口号转换为主机端口号
uint16_t ntohs(uint16_t netshort);
//将点分十进制的IP地址转换为整形网络字节序IP地址
in_addr_t inet_addr(const char *cp);
//将整形网络字节序IP地址转换为点分十进制的IP地址
char *inet_ntoa(struct in_addr in);
4.udp套接字编程
(1)基本接口的认识
socket(创建套接字)
int socket(int domain, int type, int protocol);
参数:
domain:作用域,IPV4使用AF_INET,IPV6使用AF_INET6
type:套接字类型,udp使用SOCK_DGRAM,tcp使用SOCK_STREAM
protocol:使用的协议类型,UDP使用宏IPPROTO_UDP,tcp使用宏IPPROTO_TCP
返回值:成功返回套接字描述符,失败返回-1
bind(绑定地址信息)
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
参数:
sockfd:套接字描述符
addr:要绑定的地址结构
IPV4通信程序需要使用sockaddr_in结构体,这个结构体必须为以下三个成员赋值才能成功绑定地址信息
sin_family:协议族,使用AF_INET
sin_addr.s_addr:ip地址
sin_port:端口号
addrlen:绑定的地址结构的大小
返回值:成功返回0,失败返回-1
recvfrom(接收数据)
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
参数:
sockfd:套接字描述符
buf:发送数据的首地址
len:发送数据的长度
flags:标识位,0默认阻塞发送
addr:对端地址结构,是一个输出型参数,不获取时设置为空
addrlen:接收的对端地址结构的大小,是一个输出型参数,不获取时设置为空
返回值:成功返回接收到的数据的字节数,失败返回-1
sendto(发送数据)
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
参数:
sockfd:套接字描述符
buf:发送数据的首地址
len:发送数据的长度
flags:标识位,0默认阻塞发送
addr:对端地址结构
addrlen:对端地址结构的大小
返回值:成功返回发送的字节数,失败返回-1
close(关闭套接字)
int close(int fd);
参数:
fd:关闭的文件描述符
返回值:成功返回0,失败返回-1
(2)服务端的通信流程
- 创建套接字
在内核中创建一个socket结构体 - 绑定地址信息
在创建套接字创建的socket结构体中加入IP+port信息
告诉操作系统,主机收到的哪些数据应该交给当前的这个socket,
确定发送数据的源端地址信息 - 接收数据
从socket的接收缓冲区中取出数据 - 发送数据
将数据放到socket的发送缓冲区,内核选择合适的时候封装发送 - 关闭套接字
释放套接字结构体
注意:服务端接收数据,发送数据是循环进行的,具体流程见下面的代码
(3)客户端的通信流程
- 创建套接字
- 绑定地址信息(不推荐自己绑定,由操作系统帮我们绑定没有使用的地址)
- 发送数据
- 接收数据
- 关闭套接字
(4)封装udp类进行网络通信
UdpSocket.hpp代码:
#include <iostream>
#include <string>
#include <cstdio>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
class UdpSocket{
private:
int _sockfd;
public:
UdpSocket()
:_sockfd(-1)
{}
//创建套接字
bool Socket()
{
_sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if(_sockfd < 0)
{
perror("socket error");
return false;
}
return true;
}
//绑定地址信息
bool Bind(const std::string& ip,const uint16_t port)
{
//组织地址结构
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr=inet_addr(&ip[0]);
addr.sin_port=htons(port);
socklen_t len=sizeof(addr);
int ret=bind(_sockfd, (sockaddr*)&addr, len);
if(ret < 0)
{
perror("bind error");
return false;
}
return true;
}
//接收数据
//ip 和port 是输出型参数
bool Recv(std::string& buf, std::string* ip=nullptr, uint16_t* port=nullptr)
{
sockaddr_in peerAddr;
socklen_t len=sizeof(peerAddr);
char tmp[4096]={0};
int ret=recvfrom(_sockfd, tmp, 4095, 0, (sockaddr*)&peerAddr, &len);
if(ret < 0)
{
perror("recvfrom error");
return false;
}
buf.resize(ret);
buf=tmp;
//获得发送数据一方的ip和端口号
if(ip != nullptr)
*ip=inet_ntoa(peerAddr.sin_addr);
if(port != nullptr)
*port=ntohs(peerAddr.sin_port);
return true;
}
//发送数据
bool Send(const std::string& buf,const std::string& ip,const uint16_t port)
{
//组织地址结构
sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_addr.s_addr=inet_addr(ip.c_str());
addr.sin_port=htons(port);
int len=sizeof(addr);
int ret=sendto(_sockfd, buf.c_str(), sizeof(buf), 0, (sockaddr*)&addr, len);
if(ret < 0)
{
perror("Send error");
return false;
}
return true;
}
//关闭套接字
bool Close()
{
close(_sockfd);
return true;
}
};
服务端代码:
#include "UdpSocket.hpp"
#define CHECK_RET(q) {if(q == false) return -1;}
int main()
{
UdpSocket sock;
//创建套接字
CHECK_RET(sock.Socket());
//绑定地址信息
CHECK_RET(sock.Bind("172.19.47.151",10000));
while(1)
{
std::string buf;
std::string ip;
uint16_t port;
//接收数据
CHECK_RET(sock.Recv(buf, &ip, &port));
std::cout<<"client ["<<ip<<":"<<port<<"]say "<<buf<<std::endl;
buf.clear();
std::cout<<"server say:";
std::cin>>buf;
//发送数据
CHECK_RET(sock.Send(buf, ip, port));
}
//关闭套接字
CHECK_RET(sock.Close());
}
客户端代码:
#include "UdpSocket.hpp"
#define CHECK_RET(q) {if(q == false) return -1;}
int main()
{
//创建套接字
UdpSocket sock;
CHECK_RET(sock.Socket());
while(1)
{
std::string buf;
std::cout<<"client say: ";
std::cin>>buf;
//发送数据
CHECK_RET(sock.Send(buf, "172.19.47.151", 10000));
//接收数据
buf.clear();
CHECK_RET(sock.Recv(buf, nullptr, nullptr));
std::cout<<"server say: "<<buf<<std::endl;
}
//关闭套接字
CHECK_RET(sock.Close());
return 0;
}
5.tcp套接字编程
(1)基本接口的认识
socket(创建套接字)
int socket(int domain, int type, int protocol);
参数:
domain:作用域,IPV4使用AF_INET,IPV6使用AF_INET6
type:套接字类型,udp使用SOCK_DGRAM,tcp使用SOCK_STREAM
protocol:使用的协议类型,UDP使用宏IPPROTO_UDP,tcp使用hongIPPROTO_TCP
返回值:成功返回套接字描述符,失败返回-1
bind(绑定地址信息)
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
参数:
sockfd:套接字描述符
addr:要绑定的地址结构
IPV4通信程序需要使用sockaddr_in结构体,这个结构体必须为以下三个成员赋值才能成功绑定地址信息
sin_family:协议族,使用AF_INET
sin_addr.s_addr:ip地址
sin_port:端口号
addrlen:绑定的地址结构的大小
返回值:成功返回0,失败返回-1
connect(发起连接请求)
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
参数:
sockfd:套接字描述符
addr:要连接的地址结构
addrlen:地址结构的大小
返回值:成功返回0,失败返回-1
listen(监听)
int listen(int sockfd, int backlog);
参数:
sockfd:要监听的套接字
backlog:同一时间最大监听数目
返回值:成功返回0,失败返回-1
accept(获取新建连接)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
sockfd:监听套接字
addr:要连接的地址结构
addrlen:地址结构的大小
返回值:成功返回用于通信的套接字的文件描述符,失败返回-1
send(发送数据)
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数:
sockfd:套接字描述符
buf:发送数据的首地址
len:发送数据的大小
flags:发送方式(0表示阻塞发送)
返回值:成功返回发送的字符数,失败返回-1
recv(接收数据)
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数:
sockfd:套接字描述符
buf:接收数据的首地址
len:接收数据的大小
flags:接收方式(0表示阻塞接收)
返回值:成功返回接收到的的字符数,失败返回-1
close(关闭套接字)
int close(int fd);
参数:
fd:关闭的文件描述符
返回值:成功返回0,失败返回-1
(2)服务端的通信流程
创建套接字----->绑定地址信息------->开始监听
------->获取新建连接----->接收数据------>发送数据----->关闭套接字
注意:服务端获取新建连接,接收数据,发送数据是循环进行的,具体流程见下面的代码
(3)客户端的通信流程
创建套接字----->绑定地址信息(不推荐自己绑定,由操作系统为我们绑定)------>发起连接请求
------>发送数据------->接收数据------>关闭套接字
(4)封装一个tcp类进行网络通信
tcpSocket.hpp代码:
#include<iostream>
#include<cstdio>
#include<string>
#include<unistd.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/socket.h>
#define LISTEN_BACKLOG 10
#define CHECK_RET(q) if((q) == false){return -1;}
class TcpSocket
{
private:
int _sockfd;
public:
TcpSocket():_sockfd(-1){}
int getFd()
{
return _sockfd;
}
void setFd(int fd)
{
_sockfd=fd;
return;
}
bool Socket()
{
//int socket(地址域类型,套接字类型,协议类型)
//返回值:套接字描述符
_sockfd=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(_sockfd < 0)
{
perror("socket error");
return false;
}
return true;
}
bool Bind(const std::string& ip,const uint16_t port)
{
sockaddr_in addr;
addr.sin_family=AF_INET;//IPV4的地址结构
addr.sin_addr.s_addr=inet_addr(&ip[0]);//将点分十进制的ip地址转换为网络上的ip地址
addr.sin_port=htons(port);//将端口号转换为网络端口号
//int bind(套接字描述符,地址信息,地址长度)
int ret=bind(_sockfd,(sockaddr*)&addr,sizeof(addr));
if(ret < 0)
{
perror("bind error");
return false;
}
return true;
}
bool Listen(int backlog=LISTEN_BACKLOG)
{
//int listen(监听套接字,最大监听数)
int ret=listen(_sockfd,backlog);
if(ret < 0)
{
perror("listen error");
return false;
}
return true;
}
bool Connect(const std::string& ip,const uint16_t port)
{
sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_addr.s_addr=inet_addr(&ip[0]);
addr.sin_port=htons(port);
//int connect(套接字描述符,要连接的地址信息,地址长度)
int ret=connect(_sockfd,(sockaddr*)&addr,sizeof(addr));
if(ret < 0)
{
perror("connect error");
return false;
}
return true;
}
//获取新建连接
bool Accept(TcpSocket* sock,std::string* ip=NULL,uint16_t* port=NULL)
{
sockaddr_in addr;
socklen_t len=sizeof(addr);
//int accept(套接字描述符,地址信息,地址长度)
int newfd=accept(_sockfd,(sockaddr*)&addr,&len);
if(newfd < 0)
{
perror("accept error");
return false;
}
sock->_sockfd=newfd;
if(ip != NULL)
{
*ip=inet_ntoa(addr.sin_addr);
}
if(port != NULL)
{
*port=ntohs(addr.sin_port);
}
return true;
}
bool Recv(std::string* buf)
{
char tmp[4096]={0};
//recv(套接字描述符,用来存放接收数据的缓冲区,接受的数据大小,接受方式(0表示阻塞接收))
int ret=recv(_sockfd,tmp,4096,0);
if(ret < 0)
{
perror("recv error");
return false;
}
else if(ret == 0)
{
printf("peer shutdown");
return false;
}
buf->assign(tmp,ret);//将tmp中ret个字节大小的数据存放到buf中
return true;
}
bool Send(const std::string& data)
{
int total=0;
while(total < data.size())
{
//send(套接字描述符,发送的数据的地址,发送数据的大小,发送方式(0表示阻塞发送))
int ret=send(_sockfd,&data[0]+total,data.size()-total,0);
if(ret < 0)
{
perror("send error");
return false;
}
total+=ret;
}
return true;
}
bool Close()
{
if(_sockfd != -1)
close(_sockfd);
return true;
}
};
服务端
#include"tcpSocket.hpp"
int main(int argc,char* argv[])
{
if(argc != 3)
{
std::cout<<"usage:./server ip port"<<std::endl;
return -1;
}
std::string srvip=argv[1];
uint16_t srvport=std::stoi(argv[2]);
TcpSocket lst_sock;
//1.创建套接字
CHECK_RET(lst_sock.Socket());
//2.绑定地址信息
CHECK_RET(lst_sock.Bind(srvip,srvport));
//3.开始监听
CHECK_RET(lst_sock.Listen());
while(1)
{
TcpSocket clisock;
std::string cliip;
uint16_t cliport;
//4.获取新建连接
bool ret=lst_sock.Accept(&clisock,&cliip,&cliport);
//获取新建连接失败,继续获取
if(ret == false)
{
continue;
}
//5.接发数据
std::cout<<"get newConn:"<<cliip<<":"<<cliport<<std::endl;
std::string buf;
ret=clisock.Recv(&buf);
//接受数据失败,关闭套接字,创建新的套接字继续接收
if(ret == false)
{
clisock.Close();
continue;
}
std::cout<<"client say:"<<buf<<std::endl;
buf.clear();
std::cout<<"server say:";
std::cin>>buf;
ret=clisock.Send(buf);
//发送失败,关闭套接字
if(ret == false)
{
clisock.Close();
}
}
//6.关闭套接字
lst_sock.Close();
return 0;
}
客户端
#include"tcpSocket.hpp"
int main(int argc,char* argv[])
{
if(argc != 3)
{
std::cout<<"usage: ./client ip port"<<std::endl;
}
std::string ip=argv[1];
uint16_t port=std::stoi(argv[2]);
TcpSocket sock;
//1.创建套接字
CHECK_RET(sock.Socket());
//绑定地址信息不推荐,因为操作系统会帮客户端分配ip和端口号
//2.发起连接请求
CHECK_RET(sock.Connect(ip,port));
while(1)
{
//3.接发数据
std::string buf;
std::cout<<"client say:";
std::cin>>buf;
CHECK_RET(sock.Send(buf));
buf.clear();
CHECK_RET(sock.Recv(&buf));
std::cout<<"server say:"<<buf<<std::endl;
}
//4.关闭套接字
CHECK_RET(sock.Close());
return 0;
}
当前程序运行起来存在的问题:
多个客户端与服务端通信时,服务端只能接收到客户端的一次数据。
原因及解决方案
问题出在服务端代码处,服务端循环获取新建连接、接收数据、发送数据。而这三个操作都是阻塞操作,当没有新的连接 请求到来时阻塞、当没有接收到数据时阻塞,当没有数据发送时阻塞。所以可以采用获取新建连接与接发数据分离的方式解决问题,有多进程和多线程两种解决方案
多进程服务端:
#include"tcpSocket.hpp"
#include<signal.h>
#include<sys/wait.h>
void worker(TcpSocket& clisock)
{
bool ret;
while(1)
{
std::string buf;
ret=clisock.Recv(&buf);
if(ret == false)
{
clisock.Close();
exit(0);
}
std::cout<<"client say:"<<buf<<std::endl;
buf.clear();
std::cout<<"server say:";
std::cin>>buf;
ret=clisock.Send(buf);
if(ret == false)
{
clisock.Close();
exit(0);
}
}
clisock.Close();
exit(0);
}
int main(int argc,char* argv[])
{
if(argc != 3)
{
std::cout<<"usage:./server ip port"<<std::endl;
return -1;
}
signal(SIGCHLD,SIG_IGN);
std::string srvip=argv[1];
uint16_t srvport=std::stoi(argv[2]);
TcpSocket lst_sock;
CHECK_RET(lst_sock.Socket());
CHECK_RET(lst_sock.Bind(srvip,srvport));
CHECK_RET(lst_sock.Listen());
while(1)
{
TcpSocket clisock;
std::string cliip;
uint16_t cliport;
bool ret=lst_sock.Accept(&clisock,&cliip,&cliport);
if(ret == false)
{
continue;
}
std::cout<<"get newConn:"<<cliip<<":"<<cliport<<std::endl;
pid_t pid=fork();
if(pid < 0)
{
clisock.Close();
continue;
}
else if(pid == 0)
{
worker(clisock);
}
clisock.Close();//父子进程数据独有,释放父进程的资源
}
lst_sock.Close();
return 0;
}
多线程服务端:
#include"tcpSocket.hpp"
#include<pthread.h>
void* thread_entry(void* arg)
{
TcpSocket* clisock=(TcpSocket*)arg;
while(1)
{
std::string buf;
bool ret=clisock->Recv(&buf);
if(ret == false)
{
clisock->Close();
delete clisock;
return NULL;
}
std::cout<<"client say:"<<buf<<std::endl;
buf.clear();
std::cout<<"server say:";
std::cin>>buf;
ret=clisock->Send(buf);
if(ret == false)
{
clisock->Close();
delete clisock;
return NULL;
}
}
clisock->Close();
return NULL;
}
int main(int argc,char* argv[])
{
if(argc != 3)
{
std::cout<<"usage:./server ip port"<<std::endl;
return -1;
}
std::string srvip=argv[1];
uint16_t srvport=std::stoi(argv[2]);
TcpSocket lst_sock;
CHECK_RET(lst_sock.Socket());
CHECK_RET(lst_sock.Bind(srvip,srvport));
CHECK_RET(lst_sock.Listen());
while(1)
{
TcpSocket* clisock=new TcpSocket();
std::string cliip;
uint16_t cliport;
bool ret=lst_sock.Accept(clisock,&cliip,&cliport);
if(ret == false)
{
continue;
}
std::cout<<"get newConn:"<<cliip<<":"<<cliport<<std::endl;
//子线程负责收发数据
pthread_t tid;
pthread_create(&tid,NULL,thread_entry,(void*)clisock);
pthread_detach(tid);
}
lst_sock.Close();
return 0;
}