1. 套接字的初步认识
套接字是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。 套接字是通信的基石,是支持TCP/IP协议的路通信的基本操作单元。
2.套接字的主要类型
流套接字(SOCK_STREAM)
流套接字用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复送,并按顺序接收。 流套接字使用tcp协议。
数据报套接字(SOCK_DGRAM)
数据报套接字提供一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。 数据报套接字使用udp协议。
原始套接字(SOCK_RAW)
原始套接字与标准套接字(标准套接字指的是前面介绍的流套接字和数据报套接字)的区别在于:原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送的数据必须使用原始套接字。
3.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)服务端的通信流程
创建套接字------->绑定地址信息--------> 接收数据------->发送数据------->关闭套接字 注意:服务端接收数据,发送数据是循环进行的,具体流程见下面的代码
(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;
}
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;
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;
}
4.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()
{
_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;
addr.sin_addr.s_addr=inet_addr(&ip[0]);
addr.sin_port=htons(port);
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 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 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 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};
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);
return true;
}
bool Send(const std::string& data)
{
int total=0;
while(total < data.size())
{
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;
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;
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();
}
}
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;
CHECK_RET(sock.Socket());
CHECK_RET(sock.Connect(ip,port));
while(1)
{
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;
}
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;
}
|