1. socket编程
1.1 概述
TCP是TCP/IP体系中面向连接的传输层协议,它提供全双工和可靠交付的服务。它采用许多机制来确保端到端结点之间的可靠数据传输,如采用序列号、确认重传、滑动窗口等。
首先,TCP要为所发送的每一个报文段加上序列号,保证每一个报文段能被接收方接收,并只被正确的接收一次。
其次,TCP采用具有重传功能的积极确认技术作为可靠数据流传输服务的基础。这里“确认”是指接收端在正确收到报文段之后向发送端回送一个确认(ACK)信息。发送方将每个已发送的报文段备份在自己的缓冲区里,而且在收到相应的确认之前是不会丢弃所保存的报文段的。“积极”是指发送发在每一个报文段发送完毕的同时启动一个定时器,加入定时器的定时期满而关于报文段的确认信息还没有达到,则发送发认为该报文段已经丢失并主动重发。为了避免由于网络延时引起迟到的确认和重复的确认,TCP规定在确认信息中捎带一个报文段的序号,使接收方能正确的将报文段与确认联系起来。
最后,采用可变长的滑动窗口协议进行流量控制,以防止由于发送端与接收端之间的不匹配而引起的数据丢失。这里所采用的滑动窗口协议与数据链路层的滑动窗口协议在工作原理上完全相同,唯一的区别在于滑动窗口协议用于传输层是为了在端对端节点之间实现流量控制,而用于数据链路层是为了在相邻节点之间实现流量控制。TCP采用可变长的滑动窗口,使得发送端与接收端可根据自己的CPU和数据缓存资源对数据发送和接收能力来进行动态调整,从而灵活性更强,也更合理。
1.2 tcp服务端编程
1.2.1 TCP通信的基本步骤
服务端:socket—bind—listen—while(1){—accept—recv—send—close—}---close
客户端:socket----------------------------------connect—send—recv-----------------close
1.2.2 服务器端头文件包含
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
1.2.3 socket函数
功能:生成一个套接口描述符
原型:int socket(int domain,int type,int protocol);
参数:
domain{ AF_INET:Ipv4网络协议 AF_INET6:IPv6网络协议}
type{tcp:SOCK_STREAM udp:SOCK_DGRAM}
protocol 指定socket所使用的传输协议编号,常用的协议有:IPPROTO_TCP、IPPROTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,他们分别对应TCP协议、UDP协议、STCP协议、TIPC协议,当protocol默认为0时,则根据type参数的值,自动选择协议类型;
返回值:成功则返回套接口描述符,失败返回-1。
常用实例:
int sfd = socket(AF_INET, SOCK_STREAM, 0);
if(sfd == -1)
{
perror("socket");
exit(-1);
}
1.2.4 bind函数
功能:用来绑定一个端口号和IP地址,使套接口与指定的端口号和IP地址相关联。
原型:int bind(int sockfd,struct sockaddr * my_addr,int addrlen);
参数:
sockfd为前面socket的返回值
addrlen sockaddr的结构体长度。通常是计算sizeof(struct sockaddr);
my_addr为结构体指针变量
对于不同的socket domain定义了一个通用的数据结构:
//此结构体不常用
struct sockaddr
{
unsigned short int sa_family; //调用socket()时的domain参数,即AF_INET值。
char sa_data[14]; //最多使用14个字符长度
};
//此sockaddr结构会因使用不同的socket domain而有不同结构定义, 例如使用AF_INET domain,其socketaddr结构定义便为:
struct sockaddr_in //常用的结构体
{
unsigned short int sin_family; //即为sa_family AF_INET
uint16_t sin_port; //为使用的port编号
struct in_addr sin_addr; //为IP 地址
unsigned char sin_zero[8]; //未使用
};
struct in_addr
{
uint32_t s_addr;
};
返回值:成功则返回0,失败返回-1,并设置errno,最常见的errno有以下两种:
- EACCES,被绑定的地址是受保护的地址,仅超级用户能够访问,比如如果绑定在1-1023端口的时候,就会报该错误。
- EADDRINUSE,被绑定的地址正在使用中,比如将socket绑定在一个处于TIME_WAIT状态的socket地址。
常用实例:
struct sockaddr_in my_addr; //定义结构体变量
memset(&my_addr, 0, sizeof(struct sockaddr)); //将结构体清空
//或bzero(&my_addr, sizeof(struct sockaddr));
my_addr.sin_family = AF_INET; //表示采用Ipv4网络协议
my_addr.sin_port = htons(8888); //表示端口号为8888,通常是大于1024的一个值。
//htons()用来将参数指定的16位hostshort转换成网络字符顺序
my_addr.sin_addr.s_addr = inet_addr("192.168.0.101"); // //inet_addr()用来将IP地址字符串转换成网络所使用的二进制数字,如果为INADDR_ANY,这表示服务器自动填充本机IP地址。
if(bind(sfd, (struct sockaddr*)&my_str, sizeof(struct socketaddr)) == -1)
{perror("bind");close(sfd);exit(-1);}
/*通过将my_addr.sin_port置为0,函数会自动为你选择一个未占用的端口来使用。同样,通过将my_addr.sin_addr.s_addr置为INADDR_ANY,系统会自动填入本机IP地址*/
注意:如果my_addr.sin_addr.s_addr = htonl(INADDR_ANY)时,INADDR_ANY就是指定地址为0.0.0.0的地址,它其实是表示不确定地址,一般是用于多网卡的机器上,也就是有多个IP地址,如果设置了INADDR_ANY,那么根据端口号,无论连接哪个IP地址,都是可以连接成功的。
1.2.5 listen函数
功能:使服务器的这个端口和IP处于监听状态,等待网络中某一客户机的连接请求。如果客户端有连接请求,端口就会接受这个连接。
原型:int listen(int sockfd, int backlog);
参数:
sockfd为前面socket的返回值.即sfd
backlog指定同时能处理的最大连接要求,通常为10或者5。 最大值可设至128
返回值:成功则返回0,失败返回-1
常用实例:
if(listen(sfd, 10) == -1)
{
perror("listen");
close(sfd);
exit(-1);
}
1.2.6 accept函数
功能:接受远程计算机的连接请求,建立起与客户机之间的通信连接。服务器处于监听状态时,如果某时刻获得客户机的连接请求,此时并不是立即处理这个请求,而是将这个请求放在等待队列中,当系统空闲时再处理客户机的连接请求。当accept函数接受一个连接时,会返回一个新的socket标识符,以后的数据传输和读取就要通过这个新的socket编号来处理,原来参数中的socket也可以继续使用,继续监听其它客户机的连接请求。
原型:int accept(int s,struct sockaddr * addr,int * addrlen);
参数:
s为前面socket的返回值,即sfd
addr为结构体指针变量,和bind的结构体是同种类型的,系统会把远程主机的信息(远程主机的地址和端口号信息)保存到这个指针所指的结构体中。
addrlen表示结构体的长度,为整型指针
返回值:成功则返回新的文件描述符new_fd,失败返回-1
常用实例:
struct sockaddr_in clientaddr;
memset(&clientaddr, 0, sizeof(struct sockaddr));
int addrlen = sizeof(struct sockaddr);
int new_fd = accept(sfd, (struct sockaddr*)&clientaddr, &addrlen);
if(new_fd == -1)
{
perror("accept");
close(sfd);
exit(-1);
}
printf("%s %d success connect\n",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));
注意:accept函数只是从监听队列中取出连接,而不论连接处于什么状态,网络状况有什么变化。
1.2.7 recv函数
功能:接收远端主机传来的数据,并把数据存到由参数buf 指向的内存空间
原型:int recv(int sockfd,void *buf,int len,unsigned int flags);
参数:sockfd为前面accept的返回值.即new_fd,也就是新的套接字。
buf表示缓冲区
len表示缓冲区的长度
flags通常为0
返回值:
>0 成功,返回实际接收到的字符数;
=0 连接关闭,说明recv在等待接收数据时网络中断了;
-1 表示出错;
常用实例:
char buf[512] = {0};
if(recv(new_fd, buf, sizeof(buf), 0) == -1)
{
perror("recv");
close(new_fd);
close(sfd);
exit(-1);
}
puts(buf);
注意:read函数返回值不一样,大于0 是返回字节数,等于0是读到文件末尾了,小于0则表示出现了错误,如果错误为EINTR说明是由中断引起的,如果是ECONNNERST则表示网络连接出现了问题。
1.2.8 send函数
功能:发送数据给指定的远端主机
原型:int send(int s,const void * msg,int len,unsigned int flags);
参数:s为前面accept的返回值.即new_fd
msg一般为常量字符串
len表示长度
flags通常为0
返回值:
>0 成功,返回实际传送出去的字符数,可能会少于你所指定的发送长度;
=0 连接关闭,网络中断了;
-1 表示出错;
常用实例:
if(send(new_fd, "hello", 6, 0) == -1)
{
perror("send");
close(new_fd);
close(sfd);
exit(-1);
}
注意:write函数返回小于0时,如果为EPIPE,则表示网络连接出现了问题。
1.2.9 close函数
功能:当使用完文件后若已不再需要则可使用close()关闭该文件,并且close()会让数据写回磁盘,并释放该文件所占用的资源
原型:int close(int fd);
参数:fd为前面的sfd,new_fd
返回值:若文件顺利关闭则返回0,发生错误时返回-1
常用实例:close(new_fd);
close(sfd);
注意:在多进程并发服务器中,父子进程共享套接字,有多少个进程共享该套接字,该套接字就有多少个引用计数,此时其中一个进程调用close只是关闭了当前进程的这个文件描述符,但并没有发生四次挥手,直到这个套接字的引用计数为0时,才会发生四次挥手
1.2.10 sockatmark函数
功能:判断sockfd是否处于带外标记,即判断下一个读取的数据是否含有带外数据,若含有,则调用带MSG_OOB标志的recv来读取带外数据
原型:int sockatmark(int sockfd);
参数:fd为前面的sfd,new_fd
返回值:返回1则说明下一个数据时带外数据,若返回0,则不是
1.3 tcp客户端编程
1.3.1 connect函数
功能:用来请求连接远程服务器,将参数sockfd 的socket 连至参数serv_addr 指定的服务器IP和端口号上去。
原型:int connect (int sockfd,struct sockaddr * serv_addr,int addrlen);
参数:sockfdà为前面socket的返回值,即sfd
serv_addrà为结构体指针变量,存储着远程服务器的IP与端口号信息。
addrlenà表示结构体变量的长度
返回值:成功则返回0,失败返回-1
常用实例:
struct sockaddr_in seraddr;//请求连接服务器
memset(&seraddr, 0, sizeof(struct sockaddr));
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(8888); //服务器的端口号
seraddr.sin_addr.s_addr = inet_addr("192.168.0.101"); //服务器的ip
if(connect(sfd, (struct sockaddr*)&seraddr, sizeof(struct sockaddr)) == -1)
{perror("connect");close(sfd);exit(-1);}
将上面的头文件以及各个函数中的代码全部拷贝就可以形成一个完整的例子,此处省略。
还可以不写客户端程序,使用telnet远程登录来检测我们的服务器端程序。比如我们的服务器程序在监听8888端口,我们可以用telnet 192.168.0.101 8888来查看服务端的状况。
2. tcp编程实现
2.1 使用类封装tcp的基本操作
//头文件 SxTcp.h
#ifndef __SXTCP_H__
#define __SXTCP_H__
#include <stdio.h>
#define TIMEOUT_SEC 1
#define MAX_READ_SIZE BUFSIZ
#define DEFAULT_EPOLL_FD_NUM 1024
//Tcp类
class CTcp
{
//构造函数
public:
CTcp ();
CTcp (int nSock);
virtual ~CTcp ();
//重载操作符
public:
int operator = (int);//赋值
int operator != (int) const;//不等于操作符
int operator == (int) const;//等于操作符
//公有成员函数
public:
int GetHandle () const;//取出m_nSock
int Open ();//创建socket
int Close ();//关闭监听socket
int Connect (const char *, int) const;//连接(未设置超时)
int Bind (const char *, int) const;//绑定
int Listen (int nNum) const;//监听
int Accept () const;//接受连接
int Recv(int nFd, char* buf, int nBufLen);//服务端接收
int Send(int nFd, char* buf, int nBufLen);//服务端发送
void Close (int nFd);//服务端关闭连接socket
int Send (const void *, int, int = TCP_TIMEOUT) const;//客户端发送数据
int Recv (void *, int, int = TCP_TIMEOUT) const;//客户端接收数据
static const int SOCK_ERROR;
static const int SOCK_TIMEOUT;
static const long TCP_TIMEOUT;
//私有成员变量
private:
int m_nSock;
};
#endif
//sxTcp.cpp
#include <stdio.h>
#include <sys/socket.h>
#include <unistd.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
#include <stdarg.h>
#include <arpa/inet.h>
#include <iostream>
#include <pthread.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <netdb.h>
#include <errno.h>
#include <assert.h>
#include <sys/epoll.h>
#include <signal.h>
#include "SxTcp.h"
const int CTcp::SOCK_ERROR = -100;
const int CTcp::SOCK_TIMEOUT = -101;
const long CTcp::TCP_TIMEOUT = 500000;
//构造函数
CTcp::CTcp ()
{
m_nSock = -1;
}
//构造函数
CTcp::CTcp (int p_iSock)
{
m_nSock = p_iSock;
}
//析构函数
CTcp::~CTcp ()
{
Close ();
}
/*赋值
入参:nSockfd - socket句柄
出参:赋值后的socket句柄
*/
int CTcp::operator = (int p_iSockfd)
{
//isfdtype判断nSockfd是否为指定类型,S_IFSOCK判断是否为套接字描述符,返回1是,0不是,-1出错
assert ((m_nSock == -1) && (p_iSockfd > -1) && (isfdtype (p_iSockfd, S_IFSOCK) == 1));
m_nSock = p_iSockfd;
return m_nSock;
}
/*判断两个socket句柄是否不相等
入参:n - "!="右边的socket句柄
出参:1:不相等;0:相等
*/
int CTcp::operator != (int p_iSockfd) const
{
return (m_nSock != p_iSockfd);
}
/*判断两个socket句柄是否相等
入参:n - "=="右边的socket句柄
出参:1:相等;0:不相等
*/
int CTcp::operator == (int p_iSockfd) const
{
return (m_nSock == p_iSockfd);
}
/*取出socket句柄
入参:无
出参:取出的socket句柄
*/
int CTcp::GetHandle () const
{
return m_nSock;
}
/*创建socket
入参:无
出参:1: 成功 ; 0: 失败
*/
int CTcp::Open ()
{
assert (m_nSock == -1);
//获取tcp套接字
m_nSock = socket (AF_INET, SOCK_STREAM, 0);
return (m_nSock != -1);
}
/*关闭socket
入参:无
出参:1: 成功 ; 0: 失败
*/
int CTcp::Close ()
{
if (m_nSock != -1)
{
close (m_nSock);
m_nSock = -1;
}
return 1;
}
/*连接(未设置超时),默认为阻塞IO
入参:pHost - IP地址或主机名
nPort - 端口
出参:1: 成功 ; 0: 失败
*/
int CTcp::Connect (const char *p_szHost, int p_iPort) const
{
assert ((m_nSock != -1) && p_szHost && (p_iPort > 0));
struct sockaddr_in addr;
struct hostent *phe = NULL;
memset ((void*)&addr, 0, sizeof (addr));
addr.sin_family = AF_INET;
addr.sin_port = htons (p_iPort);
if ((addr.sin_addr.s_addr = inet_addr (p_szHost)) == -1)
{
if ((phe = gethostbyname (p_szHost)) == NULL)
return 0;
memcpy ((char *)&addr.sin_addr, phe->h_addr, phe->h_length);
}
return (connect (m_nSock, (struct sockaddr *)&addr, sizeof (addr)) == 0);
}
/*绑定
入参:pIP - IP地址
nPort - 端口
出参:1: 成功 ; 0: 失败
*/
int CTcp::Bind (const char *pIP, int nPort) const
{
assert ((m_nSock != -1) && (nPort > 0));
struct sockaddr_in addr;
struct hostent *phe = NULL;
int opt=1;
if (setsockopt (m_nSock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int)) == -1)
{
return 0;
}
memset (&addr, 0, sizeof (addr));
addr.sin_family = AF_INET;
addr.sin_port = htons (nPort);
if (!pIP)
{
addr.sin_addr.s_addr = htonl (INADDR_ANY);
}
else
{
if ((addr.sin_addr.s_addr = inet_addr (pIP)) == -1)
{
if ((phe = gethostbyname (pIP)) == NULL)
return 0;
memcpy ((char *)&addr.sin_addr, phe->h_addr, phe->h_length);
}
}
return (bind (m_nSock, (struct sockaddr *)&addr, sizeof (addr)) == 0);
}
/*监听
入参:nNum - 监听数目
出参:1: 成功 ; 0: 失败
*/
int CTcp::Listen (int nNum) const
{
assert ((m_nSock != -1) && (nNum > 0));
return (listen (m_nSock, nNum) == 0);
}
/*接受连接
入参:无
出参:其他: 连接套接字句柄 ; -1: 失败
*/
int CTcp::Accept () const
{
assert (m_nSock != -1);
return (accept (m_nSock, (struct sockaddr *)NULL, NULL));
}
/*服务端接收数据
入参:pBuf - 接收缓存
nCount - 需接收字节数
出参:实际接收字节数 ,如果接收失败,返回负数,如果对方关闭,返回0
*/
int CTcp::Recv(int nFd, char* buf, int nBufLen)
{
assert(nFd != -1 );
return recv(nFd, buf, nBufLen, 0);
}
/*客户端接收数据
入参:pBuf - 接收缓存
nCount - 需接收字节数
nMicsec - socket超时值,单位:微妙,缺省:500000微妙
出参:实际接收字节数 ,如果接收失败,返回负数,如果对方关闭,返回0
*/
int CTcp::Recv (void *pBuf, int nCount, int nMicsec) const
{
assert ((m_nSock != -1) && pBuf && (nCount > 0));
int sn = 0, rn = 0;
struct timeval tvlTime;
fd_set rdfdset;
if (nMicsec >= 0)
{
tvlTime.tv_sec = nMicsec / 1000000;
tvlTime.tv_usec = abs (nMicsec - tvlTime.tv_sec * 1000000);
}
FD_ZERO (&rdfdset);
FD_SET (m_nSock, &rdfdset);
if (nMicsec > 0)
sn = select (m_nSock + 1, &rdfdset, NULL, NULL, &tvlTime);
else
sn = select (m_nSock + 1, &rdfdset, NULL, NULL, NULL);
switch (sn)
{
case -1:
return SOCK_ERROR;
case 0:
return SOCK_TIMEOUT;
}
if ((rn = read (m_nSock, pBuf, nCount)) < 0)
return SOCK_ERROR;
return rn;
}
/*服务端发送数据
入参:pBuf - 发送缓存
nCount - 需发送字节数
出参:实际发送字节数 ,如果发送失败,返回负数
*/
int CTcp::Send(int nFd, char* buf, int nBufLen)
{
assert(nFd != -1 );
return send(nFd, buf, nBufLen, 0);
}
/*客户端发送数据
入参:pBuf - 发送缓存
nCount - 需发送字节数
nMicsec - socket超时值,单位:微妙,缺省:500000微妙
出参:实际发送字节数 ,如果发送失败,返回负数
*/
int CTcp::Send (const void *pBuf, int nCount, int nMicsec) const
{
assert ((m_nSock != -1) && pBuf && (nCount > 0));
int sn = 0, wn = 0;
struct timeval tvlTime;
fd_set wtfdset;
if (nMicsec >= 0)
{
tvlTime.tv_sec = nMicsec / 1000000;
tvlTime.tv_usec = abs (nMicsec - tvlTime.tv_sec * 1000000);
}
FD_ZERO (&wtfdset);
FD_SET (m_nSock, &wtfdset);
if (nMicsec >= 0)
sn = select (m_nSock + 1, NULL, &wtfdset, NULL, &tvlTime);
else
sn = select (m_nSock + 1, NULL, &wtfdset, NULL, NULL);
switch (sn)
{
case -1:
return SOCK_ERROR;
case 0:
return SOCK_TIMEOUT;
}
if ((wn = send (m_nSock, pBuf, nCount, 0)) <= 0)
return SOCK_ERROR;
return wn;
}
void CTcp::Close (int nFd)
{
if (nFd != -1 )
{
close(nFd);
nFd = -1;
}
}
将该类编译成动态库:
g++ -g -c SxTcp.cpp -fPIC
g++ -g -o libnetwork.so SxTcp.o -shared
2.2 利用类CTcp实现基本服务端和客户端
//TestServer.cpp
#include <stdio.h>
#include <string.h>
#include "../../network/SxTcp.h"
int main()
{
CTcp tcp;
int iRet = 0;
int iFd = 0;
char buf[128] = {0};
iRet = tcp.Open();
if (iRet == 0)
{
perror("socket create failed");
return -1;
}
iRet = tcp.Bind("192.168.233.250", 6666);
if (iRet == 0)
{
perror("socket bind failed");
return -1;
}
iRet = tcp.Listen(10);
if (iRet == 0)
{
perror("socket listen failed");
return -1;
}
while(1)
{
memset(buf, 0, sizeof(buf));
iFd = tcp.Accept();
if (iFd == -1 )
{
perror("socket accept failed");
return -1;
}
iRet = tcp.Recv(iFd, buf, sizeof(buf));
if (iRet < 0 )
{
perror("recv failed");
tcp.Close(iFd);
return -1;
}
else if(iRet == 0)
{
perror("socket not connect");
tcp.Close(iFd);
return -1;
}
fprintf(stdout, "recv data is:%s\n", buf);
memset(buf, 0, sizeof(buf));
strncpy(buf, "I have redv your data,over!", sizeof(buf)-1);
iRet = tcp.Send(iFd, buf, strlen(buf));
if (iRet < 0)
{
perror("send failed");
tcp.Close(iFd);
return -1;
}
else if(iRet == 0)
{
perror("socket not connect");
tcp.Close(iFd);
return -1;
}
}
return 0;
}
//客户端
//TestClient.cpp
#include <stdio.h>
#include <iostream>
#include <string.h>
#include "../../network/SxTcp.h"
using namespace std;
int main()
{
CTcp tcp;
int iRet = 0;
int iFd = 0;
char buf[128] = {0};
iRet = tcp.Open();
if (iRet == 0)
{
perror("socket create failed");
return -1;
}
iRet = tcp.Connect("192.168.233.250", 6666);
if (iRet == 0)
{
perror("socket connect failed");
return -1;
}
while(1)
{
memset(buf, 0, sizeof(buf));
cout << "please input some string:";
cin >> buf;
iRet = tcp.Send(buf, strlen(buf));
if (iRet < 0 && errno != EAGAIN)
{
perror("send failed");
return -1;
}
else if(iRet == 0)
{
perror("connect is closed");
return -1;
}
memset(buf, 0, sizeof(buf));
iRet = tcp.Recv(buf, sizeof(buf));
if (iRet < 0 && errno != EAGAIN)
{
perror("recv failed");
return -1;
}
else if(iRet == 0)
{
perror("socket not connect");
return -1;
}
fprintf(stdout, "recv data is:%s\n", buf);
}
return 0;
}
分别编译服务端和客户端,然后发送数据,会发现客户端发送完第一次后,再第二次循环中会报recv failed。
这是因为服务端阻塞在accept了,没办法第二次接收和发送数据,那么客户端超时以后就会报错,返回负数,导致客户端退出。
当然也可以在服务端的recv和send外再加一个循环,如下:
//TestServer.cpp
#include <stdio.h>
#include <string.h>
#include "../../network/SxTcp.h"
int main()
{
CTcp tcp;
int iRet = 0;
int iFd = 0;
char buf[128] = {0};
iRet = tcp.Open();
if (iRet == 0)
{
perror("socket create failed");
return -1;
}
iRet = tcp.Bind("192.168.233.250", 6666);
if (iRet == 0)
{
perror("socket bind failed");
return -1;
}
iRet = tcp.Listen(10);
if (iRet == 0)
{
perror("socket listen failed");
return -1;
}
while(1)
{
memset(buf, 0, sizeof(buf));
iFd = tcp.Accept();
if (iFd == -1 )
{
perror("socket accept failed");
return -1;
}
while(1){
memset(buf, 0, sizeof(buf));
iRet = tcp.Recv(iFd, buf, sizeof(buf));
if (iRet < 0 )
{
perror("recv failed");
tcp.Close(iFd);
return -1;
}
else if(iRet == 0)
{
perror("socket not connect");
tcp.Close(iFd);
return -1;
}
fprintf(stdout, "recv data is:%s\n", buf);
memset(buf, 0, sizeof(buf));
strncpy(buf, "I have redv your data,over!", sizeof(buf)-1);
iRet = tcp.Send(iFd, buf, strlen(buf));
if (iRet < 0)
{
perror("send failed");
tcp.Close(iFd);
return -1;
}
else if(iRet == 0)
{
perror("socket not connect");
tcp.Close(iFd);
return -1;
}
}
}
return 0;
}
但很显然,这样就没办法接收到第二个连接了,那么怎么解决呢?往下面看。
3. 网络编程模式
上面的虽然可以实现多个客户端访问,但是仍然是阻塞模式(即一个客户访问的时候会阻塞不让另外的客户访问)。解决办法有三种,分别是多进程、多线程、异步IO。
3.1 多进程
//因为开销比较大,所以不常用
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include "../../network/SxTcp.h"
int main()
{
CTcp tcp;
int iRet = 0;
int iFd = 0;
char buf[128] = {0};
iRet = tcp.Open();
if (iRet == 0)
{
perror("socket create failed");
return -1;
}
iRet = tcp.Bind("192.168.233.250", 6666);
if (iRet == 0)
{
perror("socket bind failed");
return -1;
}
iRet = tcp.Listen(10);
if (iRet == 0)
{
perror("socket listen failed");
return -1;
}
while(1)
{
memset(buf, 0, sizeof(buf));
iFd = tcp.Accept();
if (iFd == -1 )
{
perror("socket accept failed");
return -1;
}
if (fork() == 0)
{
while(1){
memset(buf, 0, sizeof(buf));
iRet = tcp.Recv(iFd, buf, sizeof(buf));
if (iRet < 0 )
{
perror("recv failed");
tcp.Close(iFd);
return -1;
}
else if(iRet == 0)
{
perror("socket not connect");
tcp.Close(iFd);
return -1;
}
fprintf(stdout, "recv data is:%s\n", buf);
memset(buf, 0, sizeof(buf));
strncpy(buf, "I have redv your data,over!", sizeof(buf)-1);
iRet = tcp.Send(iFd, buf, strlen(buf));
if (iRet < 0)
{
perror("send failed");
tcp.Close(iFd);
return -1;
}
else if(iRet == 0)
{
perror("socket not connect");
tcp.Close(iFd);
return -1;
}
}
}
else
{
tcp.Close(iFd);
}
}
return 0;
}
3.2 多线程
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <thread>
#include "../../network/SxTcp.h"
CTcp tcp;
void ReadThread(void* arg)
{
int *pFd = (int*)arg;
int iFd = *pFd;
char buf[128] = {0};
int iRet = 0;
while(1){
memset(buf, 0, sizeof(buf));
iRet = tcp.Recv(iFd, buf, sizeof(buf));
if (iRet < 0 )
{
perror("recv failed");
tcp.Close(iFd);
break;
}
else if(iRet == 0)
{
perror("socket not connect");
tcp.Close(iFd);
break;
}
fprintf(stdout, "recv data is:%s\n", buf);
memset(buf, 0, sizeof(buf));
strncpy(buf, "I have redv your data,over!", sizeof(buf)-1);
iRet = tcp.Send(iFd, buf, strlen(buf));
if (iRet < 0)
{
perror("send failed");
tcp.Close(iFd);
break;
}
else if(iRet == 0)
{
perror("socket not connect");
tcp.Close(iFd);
break;
}
}
}
int main()
{
int iRet = 0;
int iFd = 0;
char buf[128] = {0};
iRet = tcp.Open();
if (iRet == 0)
{
perror("socket create failed");
return -1;
}
iRet = tcp.Bind("192.168.233.250", 6666);
if (iRet == 0)
{
perror("socket bind failed");
return -1;
}
iRet = tcp.Listen(10);
if (iRet == 0)
{
perror("socket listen failed");
return -1;
}
while(1)
{
memset(buf, 0, sizeof(buf));
iFd = tcp.Accept();
if (iFd == -1 )
{
perror("socket accept failed");
return -1;
}
std::thread t_read(ReadThread, (void*)&iFd);
t_read.detach();
}
return 0;
}
3.3 异步IO
异步其实就是epoll和select模式,可以看另外的两篇专门讲epoll和select的文章。
4. 使用UDP编程
4.1 UDP协议
4.1.1 概述
UDP即用户数据报协议,它是一种无连接协议,因此不需要像TCP那样通过三次握手来建立一个连接。同时,一个UDP应用可同时作为应用的客户或服务器方。由于UDP协议并不需要建立一个明确的连接,因此建立UDP应用要比建立TCP应用简单得多。
它比TCP协议更为高效,也能更好地解决实时性的问题。如今,包括网络视频会议系统在内的众多的客户/服务器模式的网络应用都使用UDP协议。
4.1.2 Udp数据包头格式
源端口占用16bit,表示应用程序通过哪个端口来发送数据包;
目的端口占用16bit,表示数据包发送给对方应用程序的哪个端口;
长度占用16bit,表示包含头部在内的udp数据包的长度;
校验占用16bit,用来检查数据包是否存在差错;
4.1.3 udp基本通信流程及函数
UDP通信流程图如下:
服务端:socket—bind—recvfrom—sendto—close
客户端:socket----------sendto—recvfrom—close
sendto()函数原型:
int sendto(int sockfd, const void *msg,int len,unsigned int flags,const struct sockaddr *to, int tolen);
该函数比send()函数多了两个参数,to表示目地机的IP地址和端口号信息,而tolen常常被赋值为sizeof (struct sockaddr)。sendto 函数也返回实际发送的数据字节长度或在出现发送错误时返回-1。
recvfrom()函数原型:
int recvfrom(int sockfd,void *buf,int len,unsigned int flags,struct sockaddr *from,int *fromlen);
from是一个struct sockaddr类型的变量,该变量保存连接机的IP地址及端口号。fromlen常置为sizeof (struct sockaddr)。当recvfrom()返回时,fromlen包含实际存入from中的数据字节数。Recvfrom()函数返回接收到的字节数或 当出现错误时返回-1,并置相应的errno。
注意:socket编程还提供了一对函数sendmsg/recvmsg用于读写数据,该对函数既可用于tcp报文,也可用于udp报文,是通用的。
4.2 UDP编程实现
Example:
//UDP的基本操作
//服务器端:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
main()
{
int sfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sfd == -1)
{
perror("socket");
exit(-1);
}
struct sockaddr_in saddr;
bzero(&saddr, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(8888);
saddr.sin_addr.s_addr = INADDR_ANY;
if(bind(sfd, (struct sockaddr*)&saddr, sizeof(struct sockaddr)) == -1)
{
perror("bind");
close(sfd);
exit(-1);
}
char buf[512] = {0};
while(1)
{
struct sockaddr_in fromaddr;
bzero(&fromaddr, sizeof(fromaddr));
int fromaddrlen = sizeof(struct sockaddr);
if(recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&fromaddr, &fromaddrlen) == -1)
{
perror("recvfrom");
close(sfd);
exit(-1);
}
printf("receive from %s %d,the message is:%s\n", inet_ntoa(fromaddr.sin_addr), ntohs(fromaddr.sin_port), buf);
sendto(sfd, "world", 6, 0, (struct sockaddr*)&fromaddr, sizeof(struct sockaddr));
}
close(sfd);
}
//客户端:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char* argv[])
{
int sfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sfd == -1)
{
perror("socket");
exit(-1);
}
struct sockaddr_in toaddr;
bzero(&toaddr, sizeof(toaddr));
toaddr.sin_family = AF_INET;
toaddr.sin_port = htons(atoi(argv[2])); //此处的端口号要跟服务器一样
toaddr.sin_addr.s_addr = inet_addr(argv[1]); //此处为服务器的ip
sendto(sfd, "hello", 6, 0, (struct sockaddr*)&toaddr, sizeof(struct sockaddr));
char buf[512] = {0};
struct sockaddr_in fromaddr;
bzero(&fromaddr, sizeof(fromaddr));
int fromaddrlen = sizeof(struct sockaddr);
if(recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&fromaddr, &fromaddrlen) == -1)
{
perror("recvfrom");
close(sfd);
exit(-1);
}
printf("receive from %s %d,the message is:%s\n", inet_ntoa(fromaddr.sin_addr), ntohs(fromaddr.sin_port), buf);
close(sfd);
}
Example2:
//UDP发送文件,先发文件大小,再发文件内容
//服务器端:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
main()
{
int sfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sfd == -1)
{
perror("socket");
exit(-1);
}
struct sockaddr_in saddr;
bzero(&saddr, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(8888);
saddr.sin_addr.s_addr = INADDR_ANY;
if(bind(sfd, (struct sockaddr*)&saddr, sizeof(struct sockaddr)) == -1)
{
perror("bind");
close(sfd);
exit(-1);
}
char buf[512] = {0};
struct sockaddr_in fromaddr;
bzero(&fromaddr, sizeof(fromaddr));
int fromaddrlen = sizeof(struct sockaddr);
if(recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&fromaddr, &fromaddrlen) == -1)
{
perror("recvfrom");
close(sfd);
exit(-1);
}
printf("receive from %s %d,the message is:%s\n", inet_ntoa(fromaddr.sin_addr), ntohs(fromaddr.sin_port), buf);
FILE* fp = fopen("1.txt","rb");
struct stat st; //用于获取文件内容的大小
stat("1.txt", &st);
int filelen = st.st_size;
sendto(sfd, (void*)&filelen, sizeof(int), 0, (struct sockaddr*)&fromaddr, sizeof(struct sockaddr));
while(!feof(fp)) //表示没有到文件尾
{
int len = fread(buf,1,sizeof(buf),fp);
sendto(sfd, buf, len, 0, (struct sockaddr*)&fromaddr, sizeof(struct sockaddr));
}
close(sfd);
}
//客户端:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#define BUFSIZE 512
int main(int argc, char* argv[])
{
int sfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sfd == -1)
{
perror("socket");
exit(-1);
}
struct sockaddr_in toaddr;
bzero(&toaddr, sizeof(toaddr));
toaddr.sin_family = AF_INET;
toaddr.sin_port = htons(atoi(argv[2]));
toaddr.sin_addr.s_addr = inet_addr(argv[1]);
sendto(sfd, "hello", 6, 0, (struct sockaddr*)&toaddr, sizeof(struct sockaddr));
char buf[BUFSIZE] = {0};
struct sockaddr_in fromaddr;
bzero(&fromaddr, sizeof(fromaddr));
int fromaddrlen = sizeof(struct sockaddr);
int filelen = 0; //用于保存文件长度
FILE* fp = fopen("2.txt","w+b");
//接收文件的长度
recvfrom(sfd, (void*)&filelen, sizeof(int), 0, (struct sockaddr*)&fromaddr, &fromaddrlen);
printf("the length of file is %d\n",filelen);
printf("Create a new file!\n");
printf("begin to reveive file content!\n");
//接收文件的内容
while(1)
{
int len = recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&fromaddr, &fromaddrlen);
if(len < BUFSIZE)
//如果接收的长度小于BUFSIZE,则表示最后一次接收,此时要用break退出循环
{
fwrite(buf,sizeof(char),len,fp);
break;
}
fwrite(buf,sizeof(char),len,fp);
}
printf("receive file finished!\n");
close(sfd);
}
5. 协议的选择
- 对数据要求高可靠性的应用需选择TCP协议,如验证、密码字段的传送都是不允许出错的,而对数据的可靠性要求不那么高的应用可选择UDP传送;
- TCP协议在传送过程中要使用三次握手、重传确认等手段来保证数据传输的可靠性。使用TCP协议会有较大的时延,因此不适合对实时性要求较高的应用,如VOIP、视频监控等。相反,UDP协议则在这些应用中能发挥很好的作用;
- 由于TCP协议的提出主要是解决网络的可靠性问题,它通过各种机制来减少错误发生的概率。因此,在网络状况不是很好的情况下需选用TCP协议(如在广域网等情况),但是若在网络状况很好的情况下(如局域网等)就不需要再采用TCP协议,而建议选择UDP协议来减少网络负荷;