从.ini配置文件读取IP进行C/S模型的TCP建链
TCP的三次握手四次握手就不在此处说明了,百度一下你就知道,本文主要记录在WIN7上开发过程中的TCP建链到断开链接的过程。
一、从配置文件 .ini 中读取IP地址和端口号
查看IP和端口号可以使用DOS命令: netstat -a
停止:Ctrl+C
图1-1
设置配置文件,建立文本文档,将后缀名改为.ini即可生产配置文件
图1-2
为什么要从配置文件中读取IP和端口号呢,原因很简单,如果建链的对端主机IP可能会有变动,直接在软件中写死的话,下次更改就得找到工程中建链的源文件目录再进行修改,这样费时费力还不讨好,坚决不干这种事........
接下来读取IP和端口到工程中,需要用到WINDOW的API函数GetPrivateProfileStringA()和GetPrivateProfileInt(),接口说明如下:
WINBASEAPI
DWORD
WINAPI
GetPrivateProfileStringA(
__in_opt LPCSTR lpAppName, //ini文件中的字段名
__in_opt LPCSTR lpKeyName, //键名,也可以称为变量
__in_opt LPCSTR lpDefault, //若果读取键值错误用该值替代
__out_ecount_part_opt(nSize, return + 1) LPSTR lpReturnedString, //读取键值存储位置
__in DWORD nSize, //键值大小
__in_opt LPCSTR lpFileName //读取文件的路径
);
WINBASEAPI
UINT
WINAPI
GetPrivateProfileIntA(
__in LPCSTR lpAppName,
__in LPCSTR lpKeyName,
__in INT nDefault,
__in_opt LPCSTR lpFileName //同上
);
代码如下:
CString path = ".\\INI\\CdmaProtoCfg.ini"; // .\\INI是在上一层文件目录中寻找INI这个文件的意思
// 读取配置文件
int ret = 0;
char m_serverIp[256] = {0};
int m_serverPort = 0;
char err[] = "err";
ret = GetPrivateProfileString("IP_PARAM", "SERVER_IP", "err", m_serverIp, sizeof(m_serverIp), path);
if (0 == memcmp(err, m_serverIp, 3))
{
printf("IP读取错误\n");
}
m_serverPort = GetPrivateProfileInt("IP_PARAM", "SERVER_PORT", 0, path);
if (0 == m_serverPort)
{
printf("PORT读取错误\n");
}
二、TCP建链
TCP的建链过程分为创建套接字建链,接收数据,发送数据,断开链接
1、创建套接字建链
1)套接字有两种:流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)。流式是一种面向连接的Socket,针对于面向 连接的TCP服务应用;数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。 Socket为了建立Socket,程序可以调用Socket函数,该函数返回一个类似于文件描述符的句柄。socket函数原型为:int socket(int domain, int type, int protocol);domain指明所使用的协议族,通常为AF_INET,表示互联网协议族(TCP/IP协议族);type参数指 定socket的类型:SOCK_STREAM 或SOCK_DGRAM,Socket接口还定义了原始Socket(SOCK_RAW),允许程序使用低层协议;protocol通常赋值0。 Socket()调用返回一个整型socket描述符,你可以在后面的调用使用它。 Socket描述符是一个指向内部数据结构的指针, 它指向描述符表入口。调用Socket函数时,socket执行体将建立一个Socket,实际上"建立一个Socket"意味着为一个Socket数据 结构分配存储空间。 Socket执行体为你管理描述符表。两个网络程序之间的一个网络连接包括五种信息:通信协议、本地协议地址、本地主机端口、远端主机地址和远端协议端口。Socket数据结构中包含这五种信息。 socket在测量软件中的使用也很广泛。
SOCKET socket( int af, int type, int protocol );
应用程序调用socket函数来创建一个能够进行网络通信的套接字。
第一个参数指定应用程序使用的通信协议的协议族,对于TCP/IP协议族,该参数置AF_INET;
第二个参数指定要创建的套接字类型,流套接字类型为SOCK_STREAM、数据报套接字类型为SOCK_DGRAM、原始套接字SOCK_RAW(WinSock接口并不适用某种特定的协议去封装它,而是由程序自行处理数据报以及协议首部);
第三个参数指定应用程序所使用的通信协议。
该函数如果调用成功就返回新创建的套接字的描述符,如果失败就返回 INVALID_SOCKET。套接字描述符是一个整数类型的值。每个进程的进程空间里都有一个套接字描述符表,该表中存放着套接字描述符和套接字数据结 构的对应关系。该表中有一个字段存放新创建的套接字的描述符,另一个字段存放套接字数据结构的地址,因此根据套接字描述符就可以找到其对应的套接字数据结 构。每个进程在自己的进程空间里都有一个套接字描述符表但是套接字数据结构都是在操作系统的内核缓冲里。
2)在使用WinSock库函数之前必须先调用函数WSAStartup,这个函数负责初始化动态链接库Ws2_32.dll.其定义为:
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
wVersionRequested:[IN],是一个WORD(双字节)数值,它指定了应用程序需要使用的Winsock版本.
实例:希望版本号为 1.2 可以如下:
wVersionRequested = 0x0201,或者 wVersionRequested = MAKEWORD(1,2)
MAKEWORD是一个宏定义主要由两个字节组成的WORD。
lpWSAData:[OUT],指向WSADATA数据结构的指针,该结构用于返回本机的Winsock系统实现的信息.该结构WhighVersion和wVersion两个域系统支持的最高版本,后者是系统希望调用者使用的版本.
函数成功 返回0; 否则返回错误码. 需要注意ws2_32.dll尚未初始化,是无法调用WSAGetLastError()的。
3)还得了解一下struct sockaddr_in这个结构,在vs2010里可以看到它的定义如下:
typedef struct sockaddr_in {
#if(_WIN32_WINNT < 0x0600)
short sin_family;
#else //(_WIN32_WINNT < 0x0600)
ADDRESS_FAMILY sin_family;
#endif //(_WIN32_WINNT < 0x0600)
USHORT sin_port; //端口号
IN_ADDR sin_addr; //网络地址
CHAR sin_zero[8]; //为了保证结构体和struct sockaddr_in的大小 //和结构体struct sockaddr的大小相等
} SOCKADDR_IN, *PSOCKADDR_IN;
更详细的解释可以查看这个博客http://www.cnblog.com/hnrainll/archive/2011/04/24/2026432.html
代码如下:
WSADATA WSAData;
if(WSAStartup(MAKEWORD(2, 2), &WSAData))
{
printf("WSAStartup failed!\n");
WSACleanup();
return -1;
}
//Tcp建链
struct sockaddr_in serv_addr;
TCPConnect *tcp; //自定义的一个tcp类,具体细节就不体现了
tcp->m_connected = false;
tcp->m_clicentSocket = INVALID_SOCKET;
for (;;)
{
if (!tcp->m_connected)
{
memset((void *)&serv_addr, 0x00, sizeof(sockaddr_in));
if((tcp->m_clientSocket = socket(AF_INET ,SOCK_STREAM, 0)) == INVALID_SOCKET)
{
sprintf("TCP 套接字创建失败\n"); //创建套接字函数socket()
}
serv_addr.sin_family = AF_INET; //协议簇
serv_addr.sin_port = htons(tcp->m_serverPort); //小尾端转换
serv_addr.sin_addr.S_un.S_addr = inet_addr(tcp->m_serverIp);//点分十进制 转 二进制
if (connect(tcp->m_clientSocket, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == SOCKET_ERROR)
{
Sprintf("链接服务器失败\n");
tcp->m_clientSocket = INVALID_SOCKET;
}
else
{
tcp->m_connected = true;
Sprintf("链接服务器成功\n");
}
}
Sleep(2000); //隔两秒检查一次是否断链
}
2、接收数据
先上代码再说明
void RecvThread(void)
{
char buf[MAX_RECV_SIZE] = {0};
int len = 0;
fd_set fd_reads; //注释1
TCPConnect *tcp = (TCPConnect *)param;
for (;;) //数据需要一直发送,建立死循环来实现
{
FD_ZERO(&fd_reads); //同注释1
if (false == tcp->m_connected)
{
Sleep(1);
continue;
}
else
{
FD_SET(tcp->m_clientSocket, &fd_reads);
}
if (select(0, &fd_reads, NULL, NULL, NULL) > 0)
{
memset(buf, 0x00, MAX_RECV_SIZE);
len = recv(tcp->m_clientSocket, buf, MAX_RECV_SIZE, 0); //注释2
if (len > 0)
{
tcp->RecvDataProc(buf, len);
}
}
}
}
注释1:在WinSock2.h中fd_set 的定义如下:
typedef struct fd_set {
u_int fd_count; /* how many are SET? */
SOCKET fd_array[FD_SETSIZE]; /* an array of SOCKETs */
} fd_set;
它是一组文件描述字的集合,通过四个洪对它进行操作;
fd_set set;
FD_ZERO(&set);
FD_SET(fd, &set);
FD_CLR(fd, &set);
FD_ISSET(fd, &set);
可以通过这个博文来了解更细致的内容http://blog.sina.com.cn/s/blog_5c8d13830100erzs.html
注释2:在WinSock2.h中recv(SOCKET s, char FAR *buf, int len, int flags)函数 的定义如下:
#if INCL_WINSOCK_API_PROTOTYPES
WINSOCK_API_LINKAGE
int
WSAAPI
recv(
__in SOCKET s,
__out_bcount_part(len, return) __out_data_source(NETWORK) char FAR * buf,
__in int len,
__in int flags
);
#endif /* INCL_WINSOCK_API_PROTOTYPES */
不论是客户还是服务器应用程序都用recv函数从TCP连接的另一端接收数据。
该函数的第一个参数指定接收端套接字描述符;
第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
第三个参数指明buf的长度;
第四个参数一般置0。
更加细致的操作过程注解可以参考http://www.cnblogs.com/jianqiang2010/archive/2010/08/20/1804598.html
3、发送数据
void SendThread(void *param)
{
TCPConnect *tcp = (TCPConnect *)param;
for (;;)
{
if (false == tcp->m_connected)
{
continue;
}
T_TcpData tempData; //自定义结构,存储要发送的数据
if (tcp->m_sendQ->Pop((char *) &tempData) > 0) //初始化发送数据
{
int ret = send(tcp->m_clientSocket, tempData.data, tempData.datasize, 0); //注释3
Sleep(150); //如果有连包处理机制则可以去掉本行代码
}
else
{
Sleep(1);
}
}
}
注释1:在WinSock2.h中send( SOCKET s, const char FAR *buf, int len, int flags)函数 的定义如下
#if INCL_WINSOCK_API_PROTOTYPES
WINSOCK_API_LINKAGE
int
WSAAPI
send(
__in SOCKET s,
__in_bcount(len) const char FAR * buf,
__in int len,
__in int flags
);
#endif /* INCL_WINSOCK_API_PROTOTYPES */
该函数的第一个参数指定发送端套接字描述符;
第二个参数指明一个存放应用程序要发送数据的缓冲区;
第三个参数指明实际要发送的数据的字节数;
第四个参数一般置0。
更细致的解释在注释2中提到的博文中有。
4、断链
void Close()
{
closesocket(m_clientSocket); //注释4
m_clientSocket = INVALID_SOCKET;
m_connected = false;
}
注释4:在WinSock2.h中closesocket(SOCKET s)函数 的定义如下
#if INCL_WINSOCK_API_PROTOTYPES
WINSOCK_API_LINKAGE
int
WSAAPI
closesocket(
__in SOCKET s
);
#endif /* INCL_WINSOCK_API_PROTOTYPES */
这个函数关闭一个套接口。更确切地说,它释放套接口描述字s,以后对s的访问均以WSAENOTSOCK错误返回。若本次为对套接口的最后一次访问,则相应的名字信息及数据队列都将被释放。closesocket()的语义受SO_LINGER与SO_DONTLINGER选项影响,对比如下:
选项 间隔 关闭方式 等待关闭与否
SO_DONTLINGER 不关心 优雅 否
SO_LINGER 零 强制 否
SO_LINGER 非零 优雅 是
若设置了SO_LINGER(亦即linger结构中的l_onoff域设为非零),并设置了零超时间隔,则closesocket()不被阻塞立即执行,不论是否有排队数据未发送或未被确认。这种关闭方式称为“强制”或“失效”关闭,因为套接口的虚电路立即被复位,且丢失了未发送的数据。在远端的recv()调用将以WSAECONNRESET出错。
若设置了SO_LINGER并确定了非零的超时间隔,则closesocket()调用阻塞进程,直到所剩数据发送完毕或超时。这种关闭称为“优雅的”关闭。请注意如果套接口置为非阻塞且SO_LINGER设为非零超时,则closesocket()调用将以WSAEWOULDBLOCK错误返回。
若在一个流类套接口上设置了SO_DONTLINGER(也就是说将linger结构的l_onoff域设为零;则closesocket()调用立即返回。但是,如果可能,排队的数据将在套接口关闭前发送。请注意,在这种情况下WINDOWS套接口实现将在一段不确定的时间内保留套接口以及其他资源,这对于想用所以套接口的应用程序来说有一定影响。
返回值:
如无错误发生,则closesocket()返回0。否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。
到这里就完成了TCP从建立连接到释放连接的整个过程了,但需要注意的是接收数据后的存放处理,接收TCP数据的连包处理和发送数据前的数据处理这些我并没有做过多解释,都是比较简单的问题,根据你的协议规则走就行了,接收数据存在连包的情况的话可以参考下面给出的一个解决方案,有兴趣的话可以自己试试看
// 缓存偏移
memmove(m_recvBuffer.buffer, m_recvBuffer.buffer + dataLen, m_recvBuffer.pos - dataLen);
m_recvBuffer.pos -= dataLen;
memset(m_recvBuffer.buffer + m_recvBuffer.pos, 0, RECV_BUF_SIZE - m_recvBuffer.pos);
文中有引用他人的博文记录,如有侵权请及时联系。
在记录中提升,不断前行。