文章目录
基于udp协议的socket客户端和服务端的编程(接口):
int socket(int domain, int type, int protocol);//创建套接字
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);//为套接字绑定地址信息
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendto(int sockfd, const void *buf, size_t len,int flags,const struct sockaddr *dest_addr,
socklen_taddrlen);
int close(int fd);//关闭套接字
客户端:
<1>.创建套接字,使进程和网卡之间建立关联
<2>.为套接字绑定地址信息,发送数据的时候能够表述数据从哪个地址端口发送出去,对方回复数据就会恢复到这个地址端口上
<3>.发送数据,将data中dlen长度的数据通过socketfd对应的socket结构体中的ip/端口将数据发送到dest_addr地址的主机上
buf:要发送的数据
len:发送的数据的长度
flag:标志,0表示默认阻塞
const struct sockaddr *dest_addr:对端地址信息
socklen_t addrlen:对端地址长度
<4>.接收数据,从socketfd对应的socket结构体的接收队列中取出一条数据放到buf中
服务端:
<1>.创建套接字,domain:地址域,AF_INET(ipv4)地址域
type:套接字类型 SOC_STREAM流式套接字 SOC_DGRAM数据报套接字
proto:传输层协议类型 0:默认 IPPROTO_TCP 6 IPPROTO_UDP 17
返回值:套接字操作句柄---文件描述符
<2>.为套接字绑定地址信息,int bind(int sockfd, const struct sockaddr * addr,socklen_t addrlen);
sockfd:创建好的套接字操作句柄
struct sockaddr:用同一的结构体,用户自己传入sa_family,16位地址类型,从而决定使用哪种解析方式,实现统一管理
在socket结构体中描述符这个socket处理哪个地址和端口的数据
网卡把数据发送到socket结构体中的接收队列
服务端绑定地址的目的:告诉操作系统网卡接收到数据的时候,哪个地址和端口的数据应该放到哪个socket结构体中的接收队列
注意事项:
htons,主机字节序转换为网络字节序 uint16_t htons(uint16_t hostshort);
inet_addr,把点分十进制的字符串IP地址,转换为网络字节序IP地址 in_addr_t inet_addr(const char *cp);
struct sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_port=htons(peer_port);
addr.sin_addr.s_addr=inet_addr(peer_ip.c_str());
socklen_t len=sizeof(struct sockaddr_in);
recv成功就返回实际接收的数据长度,失败返回-1;
buf.assign(tmp,ret),把tmp中的ret个拷贝到buf中
char *inet_ntoa(struct in_addr in);把网络字节序的IP地址转换为点分十进制的字符串IP地址
ntohs,将网络字节序的16位数据,转换为主机字节序数据
客户端不推荐用户主动绑定固定地址,因为一个端口只能被一个进程占用,一旦端口固定,客户端程序只能启动一个
socket没有 绑定地址,这时候操作系统在发送数据之前检测到,操作系统会为socket选择一个合适的地址和端口进行绑定
访问只能访问公网地址,绑定的是本机地址
昨天看来是真的不适合写代码…怎么写怎么错…今天居然改着改着改对了…
代码示例:
USocket.hpp
#pragma once
#include<iostream>
#include<string>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
using namespace std;
#define CHECK_RET(q) if((q)==false){return -1;}
class Socket
{
public:
bool Sock()
{
//int socket(int domain, int type, int protocol);
_sockfd=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
if(_sockfd<0)
{
cout<<"socket error"<<endl;
return false;
}
return true;
}
bool Bind(const string &ip,const uint16_t port)
{
//int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
struct sockaddr_in addr;
addr.sin_family=AF_INET;
//uint16_t htons(uint16_t hostshort);
addr.sin_port=htons(9000);
//将点分十进制字符串IP地址转换为网络字节序格式的ip地址
addr.sin_addr.s_addr=inet_addr(ip.c_str());
int ret;
socklen_t len=sizeof(struct sockaddr_in);
ret=bind(_sockfd,(struct sockaddr*)&addr,len);
if(ret<0)
{
cout<<"bind error"<<endl;
return false;
}
return true;
}
bool Send(const string &data,const string &peer_ip,const uint16_t peer_port)
{
//ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
//const struct sockaddr *dest_addr, socklen_t addrlen);
struct sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_port=htons(peer_port);
addr.sin_addr.s_addr=inet_addr(peer_ip.c_str());
socklen_t len=sizeof(struct sockaddr_in);
int ret=sendto(_sockfd,&data[0],data.size(),0,(struct sockaddr*)&addr,len);
if(ret<0)
{
cout<<"send error"<<endl;
return false;
}
return true;
}
bool Recv(string &buf,string &peer_ip,uint16_t &peer_port)
{
//ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
//struct sockaddr *src_addr, socklen_t *addrlen);
struct sockaddr_in peer_addr;
socklen_t len=sizeof(struct sockaddr_in);
char tmp[4096]={0};
int ret=recvfrom(_sockfd,tmp,4096,0,(struct sockaddr*)&peer_addr,&len);
if(ret<0)
{
cout<<"recv error"<<endl;
return false;
}
peer_ip=inet_ntoa(peer_addr.sin_addr);
peer_port=ntohs(peer_addr.sin_port);
buf.assign(tmp,ret);
return true;
}
void Close()
{
close(_sockfd);
}
private:
int _sockfd;
};
udp-client.cpp
#include"USocket.hpp"
#include<sstream>
int main(int argc,char *argv[])
{
if(argc!=3)
{
cout<<"start wrong"<<endl;
return -1;
}
uint16_t port;
string ip=argv[1];
stringstream tmp;
tmp<<argv[2];
tmp>>port;
Socket sock;
CHECK_RET(sock.Sock());
while(1)
{
string buf;
cout<<"client say:";
cin>>buf;
sock.Send(buf,ip,port);
buf.clear();
sock.Recv(buf,ip,port);
cout<<"server say:"<<buf<<endl;
}
sock.Close();
return 0;
}
udp-server.cpp
#include"USocket.hpp"
#include<sstream>
int main(int argc,char *argv[])
{
if(argc!=3)
{
cout<<"start wrong"<<endl;
return -1;
}
uint16_t port;
string ip=argv[1];
stringstream tmp;
tmp<<argv[2];
tmp>>port;
Socket sock;
CHECK_RET(sock.Sock());
sock.Bind(ip,port);
while(1)
{
string buf;
sock.Recv(buf,ip,port);
cout<<"client say:"<<buf<<endl;
buf.clear();
cout<<"server say:";
cin>>buf;
sock.Send(buf,ip,port);
}
sock.Close();
return 0;
}
基于tcp协议的客户端与服务端通信流程:
客户端:
<1>.创建套接字
<2>.为套接字绑定地址信息
<3>.向服务端发起连接请求int connect(int sockfd, const struct sockaddr * addr,socklen_t addrlen);
SYN--->ACK+SYN--->ACK
<4>.发送数据,ssize_t send(int sockfd, const void * buf, size_t len, int flags);
<5>.接收数据,ssize_t recv(int sockfd, void * buf, size_t len, int flags);
MSG_PEEK:从接收缓冲区获取数据,但是不从缓冲区移除
<6>.关闭套接字,close(sockfd);
服务端:
<1>.创建套接字
<2>.为套接字绑定地址信息
<3>.开始监听listen(int sockfd,int backlog);
开始监听就是告诉操作系统,若是有新的客户端连接请求过来,就为这个客户端完成三次握手建立连接的过程
服务端收到客户端新的SYN连接请求,则会为这个客户端新建一个socket专门用于与这个客户端进行独立通信
backlog:客户端的最大并发连接数,已完成连接队列(默认最小为1),未完成连接队列(满了之后的新请求被丢弃)
已完成队列满,同时未完成连接队列中有完成连接的,依旧等待在未完成队列中
<4>.获取为客户端新建立连接的socket
newfd=int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
从已完成连接队列中阻塞获取一个已完成连接的socket,并且使用这个socket与指定的客户端进行数据通信
(最早创建的socket只用于获取新连接请求)
UDP是只能客户端先发送请求,但是TCP谁先发都是可以的
<5>.发送数据,ssize_t send(int newfd, const void * buf, size_t len, int flags);
<6>.接收数据,ssize_t recv(int newfd, void * buf, size_t len, int flags);
<7).关闭套接字,close(sockfd);
注意事项:
_socketfd=socket(AF_INET,SOCK_STREAM,IPROTO_TCP);
bool Bind(string &ip,string &port);
sstream tmp;tmp<<port;tmp>>addr.sin_port;
addr.sin_addr.s_addr=inet_addr(ip.c_str());
tcp的面向连接有一个三次握手建立连接的过程,使用int listen(int sockfd, int backlog);开始监听,并且完成三次握手建立连接过程
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);//addr:服务端地址信息 addrlen:地址信息长度
网络字节序的转换
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);//sockfd:监听套接字的描述符 addr:客户端地址信息
返回值为新建连接的socket描述符--->与客户端进行数据通信
发送数据使用的send接口中第一个参数sockfd,客户端是套接字描述符,服务端是新建的socket描述符,成功返回实际发送的长度,失败返回-1
最后关闭的是监听的socket
sudo netstat -anptu
-a 查看所有信息
-n 不以服务别名显示
代码示例:
tcpsocket.hpp
#pragma once
#include<iostream>
#include<sstream>
#include<stdio.h>
#include<string>
#include<unistd.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<arpa/inet.h>
using namespace std;
#define CHECK_RET(q) if((q)==false){return -1;}
class TcpSocket
{
public:
void SetFd(int fd)
{
_sockfd=fd;
}
int GetFd()
{
return _sockfd;
}
bool Socket()
{
//int socket(int domain, int type, int protocol);
_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(_sockfd<0)
{
cout<<"sock error"<<endl;
return false;
}
return true;
}
int strtoint(const string &s)
{
int num;
stringstream tmp;
tmp<<s;
tmp>>num;
return num;
}
bool Bind(const string &ip,const string &port)
{
struct sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_port=htons(strtoint(port));
addr.sin_addr.s_addr=inet_addr(ip.c_str());
socklen_t len=sizeof(struct sockaddr_in);
int ret=bind(_sockfd,(struct sockaddr*)&addr,len);
if(ret<0)
{
cout<<"bind error"<<endl;
return false;
}
return true;
}
bool Listen(const int backlog=5)
{
//int listen(int sockfd, int backlog);
int ret=listen(_sockfd,backlog);
if(ret<0)
{
cout<<"listen error"<<endl;
return false;
}
return true;
}
bool Connect(const string &srv_ip,const string &srv_port)
{
//int connect(int sockfd, sockaddr *addr,socklen_t addrlen)
struct sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_port=htons(strtoint(srv_port));
addr.sin_addr.s_addr=inet_addr(srv_ip.c_str());
socklen_t len=sizeof(struct sockaddr_in);
int ret=connect(_sockfd,(struct sockaddr*)&addr,len);
if(ret<0)
{
cout<<"connect error"<<endl;
return false;
}
return true;
}
bool Accept(TcpSocket& clientSock,string *ip=NULL,uint16_t *port=NULL)
{
struct sockaddr_in clientaddr;
socklen_t len=sizeof(struct sockaddr_in);
int newfd=accept(_sockfd,(struct sockaddr*)&clientaddr,&len);
if(newfd<0)
{
cout<<"accept error"<<endl;
return false;
}
clientSock.SetFd(newfd);
if(ip!=NULL)
{
*ip=inet_ntoa(clientaddr.sin_addr);
}
if(port!=NULL)
{
*port=ntohs(clientaddr.sin_port);
}
return true;
}
bool Send(string &data)
{
int ret=send(_sockfd,&data[0],data.size(),0);
if(ret<0)
{
cout<<"send error"<<endl;
return false;
}
return true;
}
bool Recv(string &buf)
{
char tmp[4096]={0};
int ret=recv(_sockfd,tmp,4096,0);
if(ret<0)
{
cout<<"recv error"<<endl;
return false;
}
else if(ret==0)
{
cout<<"connect shundown"<<endl;
return false;
}
buf.assign(tmp,ret);
return true;
}
void Close()
{
close(_sockfd);
}
private:
int _sockfd;
};
tcp-server.cpp
#include<iostream>
#include<stdio.h>
#include"tcpsocket.hpp"
using namespace std;
int main(int argc,char *argv[])
{
if(argc!=3)
{
cout<<"start wrong"<<endl;
return -1;
}
TcpSocket lst_sock;
CHECK_RET(lst_sock.Socket());
CHECK_RET(lst_sock.Bind(argv[1],argv[2]));
CHECK_RET(lst_sock.Listen());
while(1)
{
TcpSocket clientsock;
bool ret=lst_sock.Accept(clientsock);
if(ret==false)
{
continue;
}
string buf;
ret=clientsock.Recv(buf);
if(ret==false)
{
clientsock.Close();
continue;
}
cout<<"client say:"<<buf<<endl;
buf.clear();
cout<<"server say:"<<endl;
fflush(stdout);
cin>>buf;
clientsock.Send(buf);
}
lst_sock.Close();
return 0;
}
tcp-client.cpp
#include"tcpsocket.hpp"
#include<iostream>
using namespace std;
int main(int argc,char *argv[])
{
if(argc!=3)
{
cout<<"start wrong"<<endl;
return -1;
}
TcpSocket sock;
CHECK_RET(sock.Socket());
CHECK_RET(sock.Connect(argv[1],argv[2]));
while(1)
{
string buf;
cout<<"client say:";
fflush(stdout);
cin>>buf;
sock.Send(buf);
buf.clear();
sock.Recv(buf);
cout<<"server say:"<<buf<<endl;
}
sock.Close();
return 0;
}
Accept和Recv都会阻塞,只能与一个客户端通信一次,因为服务端不知道客户端的新连接请求/数据什么时候到来,因此在流程写死的情况下,就会阻塞,导致流程无法继续
解决方案:服务端为每一个新的客户端都创建一个进程/线程来与客户端进行通信,example:QQ收到不同的信息会打开不同的窗口,QQ主进程只有一个,但是可以创建很多线程
UDP无连接,只要知道地址,就可以发送,只使用一个socket
多进程版本:
父进程负责accept,子进程单独负责每个客户端
#include<iostream>
#include<stdio.h>
#include<signal.h>
#include<sys/wait.h>
#include"tcpsocket.hpp"
using namespace std;
void sigcb(int signo)
{
while(waitpid(-1,NULL,WNOHANG)>0);
}
int main(int argc,char *argv[])
{
if(argc!=3)
{
cout<<"start wrong"<<endl;
return -1;
}
signal(SIGCHLD,sigcb);
TcpSocket lst_sock;
CHECK_RET(lst_sock.Socket());
CHECK_RET(lst_sock.Bind(argv[1],argv[2]));
CHECK_RET(lst_sock.Listen());
while(1)
{
TcpSocket clientsock;
bool ret=lst_sock.Accept(clientsock);
if(ret==false)
{
continue;
}
if(fork()==0)
{
while(1)
{
string buf;
ret=clientsock.Recv(buf);
if(ret==false)
{
clientsock.Close();
continue;
}
cout<<"client say:"<<buf<<endl;
buf.clear();
cout<<"server say:";
fflush(stdout);
cin>>buf;
clientsock.Send(buf);
}
clientsock.Close();
}
clientsock.Close();
}
lst_sock.Close();
return 0;
}
多线程版本:
线程之间共享文件描述符表,所以不可以使用close关闭,普通线程使用完之后close和delete
每个线程负责一个任务,防止多个任务交叉造成阻塞
#include<iostream>
#include<stdio.h>
#include"tcpsocket.hpp"
using namespace std;
void* thr_start(void* arg)
{
TcpSocket *clientsock=(TcpSocket*)arg;
while(1)
{
string buf;
clientsock->Recv(buf);
cout<<"client say:"<<buf<<endl;
buf.clear();
cout<<"server say:";
fflush(stdout);
cin>>buf;
clientsock->Send(buf);
}
clientsock->Close();
delete clientsock;
return NULL;
}
int main(int argc,char *argv[])
{
if(argc!=3)
{
cout<<"start wrong"<<endl;
return -1;
}
TcpSocket lst_sock;
CHECK_RET(lst_sock.Socket());
CHECK_RET(lst_sock.Bind(argv[1],argv[2]));
CHECK_RET(lst_sock.Listen());
while(1)
{
TcpSocket *clientsock=new TcpSocket();
bool ret=lst_sock.Accept(*clientsock);
if(ret==false)
{
continue;
}
pthread_t tid;
pthread_create(&tid,NULL,thr_start,(void*)clientsock);
}
lst_sock.Close();
return 0;
}
连接断开的体现:
当通信双方,连接断开时,recv返回0(表示连接断开),应该关闭套接字
send出发异常,发送SIGPIPE信号,会导致进程退出