目录
简单对比TCP和UDP
创建套接字实质
socket应用编程接口详解(包括UDP&TCP编程接口)
基本TCP&UDP编程接口详解:
sockaddr , sockaddr_in , in_addr区别
UDP套接字编程流程&举例:
UDP编程流程:
UDP编程举例:
TCP套接字编程流程&例子:
TCP编程流程
三次握手四次挥手(重点)
TCP编程举例(普通版本 + 多进程版本 + 多线程版本)
创建套接字的实质:创建套接字实际就是在内核中创建了一个struct socket{..};在这个结构体中有两个缓冲区,一个是接受缓存区,一个是发送缓冲区,操作系统的职责实际上,是将网卡接收到的数据,进行端口辨认,拷贝到不同的socket缓存区,程序员在调用的操作系统定义的接口(sendto,recvfrom)实际上就是和socket缓存区打交道。
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *addr,socklen_t add_len);
// 开始监听socket (TCP, 服务器)
int listen(int sockfd, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int sockfd struct sockaddr* addr,socklen_t* add_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen)
//发送函数(一般用在UDP 客户端,服务端)
ssize_t sendto (int sockfd,const void *buf,int flags,const struct sockaddr *dest_adddr,sockle_t addrlen);
//接收函数 (一般用在UDP 客户端,服务端)
ssize_t recvfrom(int sockfd,void* buf.size_t len,int flags,struct sockaddr* src_addr,socklen_t addrlen) ;
//发送函数 (一般用在TCP 客户端,服务端)
ssize_t send(int sockfd, void *buf, size_t len int flags)
//接收函数 (一般使用在TCP 客户端,服务端)
ssize_t recv(int sockfd, void *buf, size_t len int flags)
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol); 包含在 sys/types.h 或者sys/socket.h
domain:地址域,传入的是协议的版本
网络层::AF_ INET --> ipv4版本的ip协议
网络层:AF_ INET6 --> ipv6版本的ip协议
type:套接字的类型
传输层: tcp/udp
SOCK_ STREAM:流式套接字--》默认对应的协议: tcp,不枝持udp
SOCK_ DGRAM:数据报套接字--》默认对应的协议: udp,不支持tcp
protocol:协议类型
0 :采套接字对应的默认类型
IPPROTO_ TCP-->6
IPPROTO_ UDP-->17
返回值:返回套接字的操作句柄,实际上就是一个文件描述符,称为套接字描述符
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *addr,socklen_t addr_len);包含在 sys/types.h 或者sys/socket.h
socket: 套接字操作句柄,上面 socket函数返回值。
addr:通用的socket地址,下面会详讲。
addr_len:地址信息长度
返回值:成功 0 失败-1
其实sockaddr 和sockaddr_in 二者的占用的内存大小是一致的,因此可以互相转化,从这个意义上说,他们并无区别。
sockaddr常用于bind、connect、recvfrom、sendto等函数的参数,指明地址信息。是一种通用的套接字地址。
sockaddr_in 是internet环境下套接字的地址形式。所以在网络编程中我们会对sockaddr_in结构体进行操作。使用sockaddr_in来建立所需的信息,最后使用类型转化就可以了。
虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP地址.一般先把sockaddr_in变量赋值后,强制类型转换后传入用sockaddr做参数的函数:sockaddr_in用于socket定义和赋值;sockaddr用于函数参数。举例:
#include
#include
#include
#include
int main(int argc,char **argv)
{
int sockfd;
struct sockaddr_in mysock;
sockfd = socket(AF_INET,SOCK_STREAM,0); //获得fd
bzero(&mysock,sizeof(mysock)); //初始化结构体
mysock.sin_family = AF_INET; //设置地址家族
mysock.sin_port = htons(800); //设置端口
mysock.sin_addr.s_addr = inet_addr("192.168.1.0"); //设置地址
bind(sockfd,(struct sockaddr *)&mysock,sizeof(struct sockaddr); /* bind的时候进行转化 */
... ...
return 0;
}
注意这里要用到之前的 主机字节序和网络字节序转换函数 ,文章末尾https://mp.csdn.net/console/editor/html/104920103
// 开始监听socket (TCP, 服务器)
int listen(int sockfd, int backlog); 包含在sys/socket.h
sockfd:指定套接字操作句柄
backlog:已经完成连接队列的大小,也是同一时刻,服务端最大并发连接数。
返回值: 0 :成功 -1:失败
注意:监听的时候一旦有新的连接到来,os会对新的连接分配一个socket,进行一对一服务。
// 接收请求 (TCP, 服务器)
int accept(int sockfd, struct sockaddr* addr,socklen_t* add_len); 包含在sys/socket.h
softfd:用来标识服务端套接字(也就是listen函数中设置为监听状态的套接字)
addr:用来保存客户端套接字对应的“地方”(包括客户端IP和端口信息等)
add_len:“地方”的占地大小即客户端地址信息的长度,输入输出型参数
返回值:返回的是操作系统内核创建的新的socket的文件描述符。所谓“新的”就是说这个套接字与socket()返回的用于监听和接受客户端的连接请求的套接字不是同一个套接字。与本次接受的客户端的通信是通过在这个新的套接字上发送和接收数据来完成的。
// 建立连接 (TCP, 客户端) 重点
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
sockfd:标识一个套接字。
addr:套接字想要连接的主机地址和端口号。
addrlen:name缓冲区的长度。返回值:成功则返回0,失败返回-1,错误原因存于errno中。
注意:
connect是套接字连接操作,connect操作之后代表对应的套接字已连接,UDP协议在创建套接字之后,可以同多个服务器端建立通信,而TCP协议只能与一个服务器端建立通信,TCP不允许目的地址是广播或多播地址,UDP允许。当然UDP协议也可以像TCP协议一样,通过connect来指定对方的ip地址、端口。
UDP协议经过connect之后,在通过sendto来发送数据报时不需要指定目的地址、端口,如果指定了目的地址、端口,那么会返回错误。通过UDP协议可以给同一个套接字指定多次connect操作,而TCP协议不可以,TCP只能指定一次connect操作。UDP协议指定第二次connect操作之后会先断口第一次的连接,然后建立第二次的连接。
//发送函数(一般用在UDP) 包含在 sys/types.h 或者sys/socket.h
ssize_t sendto (int sockfd,const void *buf,int flags,const struct sockaddr *dest_adddr,sockle_t addrlen);
sockfd:套接字的操作句柄
buf: 要发什么数据
len: 发送数据的长度
flags:一般设置为0 ,进行阻塞发送
dest_arr:目标主机的地址信息 (目的ip+目的port)
addrlen:地址信息长度
返回值:成功则返回实际传送出去的字符数, 失败返回-1, 错误原因存于errno 中.
//接收函数 (一般用在UDP) 包含在 sys/types.h 或者sys/socket.h
ssize_t recvfrom(int sockfd,void* buf.size_t len,int flags,struct sockaddr* src_addr,socklen_t addrlen) ;
sockfd: 套接字操作句柄
buf:从接收缓冲区拿到的数据存到哪里
len:接收buf定义的最大长度,预留‘\0'位置
flags: 0:阻塞接收
src_addr:源主机的地址信息(ip+port)
addrlen:地址信息长度,输入输出型参数,作为入参,指定传入源主机地址信息结构体的长度,作为出参将实际地址信息的的长度返回来。
返回值:返回接收到的字节数或当出现错误时返回-1,并置相应的errno。
注意:sendto和recvfrom一般用于UDP协议中,但是如果在TCP中connect函数调用后也可以用.
//发送函数 (一般使用在TCP) 包含在sys/socket.h
ssize_t send(int sockfd, void *buf, size_t len int flags)
sockfd:accept函数返回的操作系统新创建的socket 操作句柄
buf:给对端发的数据
len:发送数据的长度
flags: 在这里取 0 ,阻塞发送
//接收函数 (一般使用在TCP) 包含在sys/types.h 或sys/socket.h
ssize_t recv(int sockfd, void *buf, size_t len int flags)
sockfd :accept函数的返回值
buf: 从接收缓存区接收的数据放到哪里去
len: 最大可存放多少
flags: 0:阻塞接收 MSG_PEEK:探测接收
接收数据时不会将接收缓存区的数据擦除掉,而是拷贝接收缓存区的数据(接收缓冲区当中还是原有的数据)
//关闭函数 (UDP/TCP)
close(int sockfd) 包含在:unistd.h
sockfd: 套接字操作句柄
调用成功返回0,否则返回-1并设置errno;
UDP通信必须客户端先发数据,服务端拿到客户端地址信息,两边才能进行互相通信。
下面我们按照上面步骤实现一个小的UDP通信小程序
udpsver.hpp
#include
#include
#include
#include
#include
#include
#include
class UdpSvr
{
public:
UdpSvr()
{
Sock_ = -1;
}
~UdpSvr()
{
}
bool CreateSock()
{
Sock_ = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if(Sock_ < 0)
{
perror("socket");
return false;
}
return true;
}
bool Bind(std::string& ip, uint16_t port)
{
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
int ret = bind(Sock_, (struct sockaddr*)&addr, sizeof(addr));
if(ret < 0)
{
perror("bind");
return false;
}
return true;
}
bool Send(std::string& buf, struct sockaddr_in* destaddr)
{
int SendSize = sendto(Sock_, buf.c_str(), buf.size(), 0, (struct sockaddr*)destaddr, sizeof(struct sockaddr_in));
if(SendSize < 0)
{
perror("sendto");
return false;
}
return true;
}
bool Recv(std::string& buf, struct sockaddr_in* srcaddr)
{
char tmp[1024] = {0};
socklen_t socklen = sizeof(struct sockaddr_in);
int RecvSize = recvfrom(Sock_, tmp, sizeof(tmp) - 1, 0, (struct sockaddr*)srcaddr, &socklen);
if(RecvSize < 0)
{
perror("recvfrom");
return false;
}
buf.assign(tmp, RecvSize);
return true;
}
void Close()
{
close(Sock_);
Sock_ = -1;
}
private:
int Sock_;
};
svr.cpp
#include "udpsver.hpp"
//./svr ip port
int main(int argc, char* argv[])
{
if(argc != 3)
{
printf("./svr [ip] [port]\n");
return 0;
}
std::string ip = argv[1];
uint16_t port = atoi(argv[2]);
UdpSvr us;
if(!us.CreateSock())
{
return 0;
}
if(!us.Bind(ip, port))
{
return 0;
}
while(1)
{
//接收数据,发送数据
std::string buf;
struct sockaddr_in peeraddr;
us.Recv(buf, &peeraddr);
printf("client say:%s\n", buf.c_str());
printf("server say:");
fflush(stdout);
std::cin >> buf;
us.Send(buf, &peeraddr);
}
us.Close();
return 0;
}
cli.cpp
#include "udpsver.hpp"
int main(int argc, char* argv[])
{
if(argc != 3)
{
printf("./cli [ip] [port]\n");
return 0;
}
std::string ip = argv[1];
uint16_t port = atoi(argv[2]);
UdpSvr us;
if(!us.CreateSock())
{
return 0;
}
//if(!us.Bind(ip, 19998))
//{
// return 0;
//}
struct sockaddr_in destaddr;
destaddr.sin_family = AF_INET;
destaddr.sin_addr.s_addr = inet_addr(ip.c_str());
destaddr.sin_port = htons(port);
while(1)
{
//发送数据,接收数据
std::string buf;
printf("client say:");
fflush(stdout);
std::cin >> buf;
us.Send(buf, &destaddr);
us.Recv(buf, &destaddr);
printf("server say:%s\n", buf.c_str());
}
us.Close();
return 0;
}
注意:
其中三次握手分为:三次握手建立连接是在监听阶段完成的。
对应的四次挥手为:
tcpsver.hpp
#include
#include
#include
#include
#include
#include
#include
//创建套接字
//绑定地址信息
//客户端--连接接口
//监听
//获取新连接
//发送数据
//接收数据
//关闭套接字
class TcpSvr
{
public:
TcpSvr()
{
Sockfd_ = -1;
}
~TcpSvr()
{
}
bool CreateSock()
{
Sockfd_ = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(Sockfd_ < 0)
{
perror("socket");
return false;
}
return true;
}
bool Bind(std::string& ip, uint16_t port)
{
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
int ret = bind(Sockfd_, (struct sockaddr*)&addr, sizeof(addr));
if(ret < 0)
{
perror("bind");
return false;
}
return true;
}
bool Listen(int BackLog = 5)
{
int ret = listen(Sockfd_, BackLog);
if(ret < 0)
{
perror("listen");
return false;
}
return true;
}
//连接接口 是对 客户端而言的 客户端需要知道连接的服务端的ip地址和port信息
bool Connect(std::string& ip, uint16_t port)
{
struct sockaddr_in destaddr;
destaddr.sin_family = AF_INET;
destaddr.sin_port = htons(port);
destaddr.sin_addr.s_addr = inet_addr(ip.c_str());
int ret = connect(Sockfd_, (struct sockaddr*)&destaddr, sizeof(destaddr));
if(ret < 0)
{
perror("connect");
return false;
}
return true;
}
//ts.send() ts.recv()
bool Accept(TcpSvr& ts, struct sockaddr_in* addr = NULL)
{
struct sockaddr_in peeraddr;
socklen_t addrlen = sizeof(struct sockaddr_in);
int NewSockFd = accept(Sockfd_, (struct sockaddr*)&peeraddr, &addrlen);
if(NewSockFd < 0)
{
return false;
}
ts.Sockfd_ = NewSockFd;
if(addr != NULL)
{
memcpy(addr, &peeraddr, addrlen);
}
return true;
}
bool Send(std::string& buf)
{
int ret = send(Sockfd_, buf.c_str(), buf.size(), 0);
if(ret < 0)
{
perror("send");
return false;
}
return true;
}
bool Recv(std::string& buffer)
{
char buf[1024] = {0};
int ret = recv(Sockfd_, buf, sizeof(buf) - 1, 0);
if(ret < 0)
{
perror("recv");
return false;
}
else if(ret == 0)
{
//对端将连接关闭了
printf("peer close this connect\n");
return false;
}
buffer.assign(buf, ret);
return true;
}
void Close()
{
close(Sockfd_);
Sockfd_ = -1;
}
private:
int Sockfd_;
};
sver.cpp
#include "tcpsver.hpp"
int main(int argc, char* argv[])
{
if(argc != 3)
{
printf("./svr [ip] [port]\n");
return 0;
}
std::string ip = argv[1];
uint16_t port = atoi(argv[2]);
TcpSvr ts;
if(!ts.CreateSock())
{
return 0;
}
if(!ts.Bind(ip, port))
{
return 0;
}
if(!ts.Listen())
{
return 0;
}
TcpSvr peerts;
struct sockaddr_in peeraddr;
if(!ts.Accept(peerts, &peeraddr))
{
return 0;
}
printf("svr have a new connect, ip:port --> %s:%d\n",
inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
while(1)
{
/*
//TcpSvr peerts;
//struct sockaddr_in peeraddr;
//if(!ts.Accept(peerts, &peeraddr))
//{
// return 0;
//}
//printf("svr have a new connect, ip:port --> %s:%d\n",
//inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));*/
std::string buf;
peerts.Recv(buf);
printf("client say:%s\n", buf.c_str());
printf("server say:");
fflush(stdout);
std::cin >> buf;
peerts.Send(buf);
}
//peerts.Close();
ts.Close();
return 0;
}
cli.cpp
#include "tcpsvr.hpp"
int main(int argc, char* argv[])
{
if(argc != 3)
{
printf("./cli [ip] [port]\n");
return 0;
}
std::string ip = argv[1];
uint16_t port = atoi(argv[2]);
TcpSvr ts;
if(!ts.CreateSock())
{
return 0;
}
if(!ts.Connect(ip, port))
{
return 0;
}
while(1)
{
printf("client say:");
fflush(stdout);
std::string buf;
std::cin >> buf;
ts.Send(buf);
ts.Recv(buf);
printf("server say:%s\n", buf.c_str());
}
ts.Close();
return 0;
}
这里我们还想在重开一个cli客户端,发现失败,拒绝服务端连接 。
这是因为在sver.cpp服务端程序中我们将下面代码放在了while循环之外。
TcpSvr peerts;
struct sockaddr_in peeraddr;
if(!ts.Accept(peerts, &peeraddr))
{
return 0;
}
printf("svr have a new connect, ip:port --> %s:%d\n",
inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
对于单个执行流来说,由于sver.cpp代码时串行运行的:
那么如何解决TCP单个执行流带来的,只能处理一个客户端的问题?
其实可以创建多个执行流,让新创建的执行流拿着新创建的socket和客户端进行业务数据沟通。也就是说每获取一个连接,就为该连接创建一个执行流,让新创建的执行流为客户端服务。
创建执行流的方式:
多进程服务端tcpProcess.cpp ,客户端还和上面cli.cpp一样:
#include"tcpsvr.hpp"
#include
#include
void sigcb(int signo)
{
(void)signo;
while(1)
{
waitpid(-1,NULL,WNOHANG);
}
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
printf("./tcpprocess [ip] [port]\n");
return 0;
}
//防止子进程意外退出,变成僵尸进程,更改sigchld处理方式
signal(SIGCHLD,sigcb);
std::string ip = argv[1];
u_int16_t port = atoi(argv[2]);
TcpSvr ts;
if(!ts.CreateSock())
{
return 0;
}
if(!ts.Bind(ip,port))
{
return 0;
}
if(!ts.Listen(5))
{
return 0;
}
while(1)
{
TcpSvr peerts;
struct sockaddr_in peeraddr;
if(!ts.Accept(peerts,&peeraddr))
{
//不能使用return;因为有可能一个客户端被接收失败,还有可能接收其他客户端
//return 0;
continue;
}
printf("Have a new connnection %s : %d\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));
int pid = fork();
if(pid < 0)
{
perror("fork");
exit(1);
}
else if(pid== 0)
{
//child
while(1)
{
std::string buf;
peerts.Recv(buf);
printf("client say:%s\n",buf.c_str());
printf("sver say:");
fflush(stdout);
std::cin>>buf;
peerts.Send(buf);
}
peerts.Close();
exit(1);
}
else
{
//father
peerts.Close();
}
}
return 0;
}
从这张图就可以看到上面说过的侦听socket 和 OS会对新的连接分配的socket
多线程服务端svrthread.cpp,客户端还和上面cli.cpp一样
#include"tcpsvr.hpp"
#include
#include
void* ThreadStart(void* arg)
{
pthread_detach(pthread_self());
TcpSvr* ts = (TcpSvr*)arg;
while(1)
{
std::string buf;
ts->Recv(buf);
printf("recv cli say:%s\n",buf.c_str());
printf("svr send say:");
fflush(stdout);
std::cin>>buf;
ts->Send(buf);
}
delete ts;
ts->Close();
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
printf("./tcpprocess [ip] [port]\n");
return 0;
}
std::string ip = argv[1];
u_int16_t port = atoi(argv[2]);
TcpSvr ts;
if(!ts.CreateSock())
{
return 0;
}
if(!ts.Bind(ip,port))
{
return 0;
}
if(!ts.Listen(5))
{
return 0;
}
while(1)
{
TcpSvr* peerts = new TcpSvr();
struct sockaddr_in peeraddr;
if(!ts.Accept(peerts,&peeraddr))
{
//不能使用return;因为有可能一个客户端被接收失败,还有可能接收其他客户端
//return 0;
continue;
}
printf("Have a new connnection %s : %d\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));
pthread_t tid;
int ret = pthread_create(&tid,NULL,ThreadStart,(void*)peerts);
if(ret<0)
{
perror("pthread_create");
return 0;
}
}
//关闭侦听套接字
ts.Close();
return 0;
}
tcpsvr.cpp和之前基本一样只是在accept函数那里做了一点改变,其余不变。
bool Accept(TcpSvr*ts,struct sockaddr_in* addr = NULL)
{
struct sockaddr_in peeraddr;//对端
socklen_t addrlen = sizeof(struct sockaddr_in);
int NewSockFd=accept(Sockfd_,(struct sockaddr*)&peeraddr,&addrlen);
//Sockfd_ :侦听socket
//NewSockFd : 针对客户端1对1服务的socket
//接收缓存区和发送缓存区进行读写:
//使用NewSockFd 调用recv(NewSockfd,buf,len,flags); 调用 send(NewSockfd,buf,len,flags);
if(NewSockFd < 0)
{
perror("accept");
return false;
}
ts->Sockfd_ = NewSockFd;
if(addr != NULL)
{
memcpy(addr,&peeraddr,addrlen);
}
return true;
}