@TOC
第一章 Socket快速入门篇
1、TCP/IP模型
用Wireshark抓包工具来看一下上图TCP/IP模型这种4层协议里面究竟有什么内容。
在windows和Linux系统之间配置共享
首先保证我们的putty已经连接上了linux服务器,然后我们要安装samba这么一个目录共享工具:
apt-get install samba
where samba
vim /etc/samba/smb.conf
移动该配置项最后,添加内容:
[code] #这就表示你这个对外部显示的共享目录的名字
path=/code #我们在根目录下面创建的code目录
writeable=yes
browseable=yes
guest ok = yes
pkill smbd //如果当前smbda已经启动了,先把它杀掉
smbd //重新启动
ps -ef //这只是当前控制端的进程列表
ps -ef | grep smbd
竖线表示一个管道,就是把前面命令的输出传给后面,后面再做处理、过滤输出
mkdir /code
chmod 777 /code //这是偷懒的办法,更保险的是我们看一下目录当前的权限
ls -l | grep code //这会把每一行的输出进行过滤,匹配之后才输出
第一个root和第二个root表示这个目录文件所属的用户和所属的组,最左边是权限部分:
d表示它是一个目录,没有的话可能是文件,字母l表示链接;
d后面有9个字母,每3个字母表示一组权限,第一组3个字母rwx
表示拥有者用户(owner)的权限,就是这个文件是由谁来创建的,像上图这里显示的是root用户创建的;
中间3个字母r-x
表示同组用户(group)的权限;
r-x
最后3个字母表示其他人(other)的权限。
我们的目录共享是以一个默认用户进来的,所以说它应该是其他用户,所以我们看到,这个code只能被其他用户读和执行,但是不可以写,而我们是要在windows上编辑linux上的文件,那它就必须要具有写的权限。
我们有两个方案,一个方案就是给这个目录设置3组权限都为rwx:
chmod 777 code
还有一个方案就是给这个目录指定用户可以访问:
我们设定这个用户和这个组可以访问这个目录。
我们先把这个共享目录code删除,再看看从windows上访问该目录的效果:
我们按键盘上的窗口键+r打开运行窗口,输入\\192.168.3.69
打开linux服务器共享的目录:
这就说明我们访问该目录没有权限。
这样就可以在windows上编辑文件、创建目录了,还可以通过svn或git提交,都可以在windows上执行了。
系统socket库介绍
2、windows上加载socket库、并创建socket
//main.cpp
//
#include <windows.h>
int main(int argc, char* argv[])
{
WSADATA ws;
WSAStartup(MAKEWORD(2, 2), &ws);
return 0;
}
设置项目属性,链接器、输入,ws2_32.lib;
编译成功!
我们调用socket函数:
//main.cpp
//
#include <windows.h>
#include <stdio.h>
int main(int argc, char* argv[])
{
WSADATA ws;
WSAStartup(MAKEWORD(2, 2), &ws);
for(int i = 0; i < 1000; i++)
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock == -1)
{
printf("create socket failed! sock = %d\n", sock);
return -1;
}
printf("sock = [%d]\n", sock);
closesocket(sock);
}
getchar();
return 0;
}
第一个参数,是用什么协议,AF_INET,就是TCP/IP协议;
第二个参数,传输层用udp还是tcp,这里我们用tcp的;
返回值,socket句柄的值。
3、移植到Linux并设置单进程创建socket的最大数量
//main.cpp
//
#ifdef _WIN32
#include <windows.h>
#endif
#include <stdio.h>
int main(int argc, char* argv[])
{
//在linux当中socket不需要初始化
#ifdef _WIN32
WSADATA ws;
WSAStartup(MAKEWORD(2, 2), &ws);
#endif
for(int i = 0; i < 1000; i++)
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock == -1)
{
printf("create socket failed! sock = %d\n", sock);
return -1;
}
printf("sock = [%d]\n", sock);
closesocket(sock);
}
getchar();
return 0;
}
make main
编译如果报错,根据信息可以判断是由于没有添加响应头文件导致的,那么socket相关的头文件是什么呢,我们通过man手册来查找。
man socket
//main.cpp
//
#ifdef _WIN32
#include <windows.h>
#else
#include <sys/types.h>
#include <sys/socket.h>
#endif
#include <stdio.h>
int main(int argc, char* argv[])
{
//在linux当中socket不需要初始化
#ifdef _WIN32
WSADATA ws;
WSAStartup(MAKEWORD(2, 2), &ws);
#endif
for(int i = 0; i < 2000; i++)
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock == -1)
{
printf("create socket failed! sock = %d\n", sock);
return -1;
}
printf("sock = [%d]\n", sock);
//closesocket(sock);
}
getchar();
return 0;
}
make main
默认会执行编译命令:g++ main.cpp -o main
可以看到在linux下,当它创建到1023的时候,再创建socket就失败了,而且我们可以看到在linux下socket是从3开始的,0、1、2是默认已经用掉的,它们分别是标准输入、标准输出、标准错误;
在linux当中,它对单个进程是文件句柄的最大数限制的,我们怎么看这个文件句柄的最大数呢?
ulimit -n
所以说,我们可以把这个值改掉,也可以把这个限制关闭掉就行了:
ulimit -n 3000
那么怎么保存这个值使之一直生效呢,我们可以把它放在启动项;
或者写一个启动脚本,在这个启动脚本当中把这个指令加进去;或者通过代码来实现。
另外,由于在windows和linux下,关闭socket的函数是不一样的:
所以我们修改代码:
//main.cpp
//
#ifdef _WIN32
#include <windows.h>
#else
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#define closesocket close
#endif
#include <stdio.h>
int main(int argc, char* argv[])
{
//在linux当中socket不需要初始化
#ifdef _WIN32
WSADATA ws;
WSAStartup(MAKEWORD(2, 2), &ws);
#endif
for(int i = 0; i < 2000; i++)
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock == -1)
{
printf("create socket failed! sock = %d\n", sock);
return -1;
}
printf("sock = [%d]\n", sock);
//closesocket(sock);
}
getchar();
return 0;
}
第二章 TCP服务器
TPC协议
- TCP协议
网络字节序
创建tcp服务端
- int accept(int sockfd, void *addr, int *addrlen);
//main.cpp
//
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#ifdef _WIN32
#include <windows.h>
#else
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#define closesocket close
#endif
int main(int argc, char* argv[])
{
//在linux当中socket不需要初始化
#ifdef _WIN32
WSADATA ws;
WSAStartup(MAKEWORD(2, 2), &ws);
#endif
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock == -1)
{
printf("create socket failed! sock = %d\n", sock);
return -1;
}
printf("sock = [%d]\n", sock);
unsigned short port = 8080;
if(argc > 1)
port = atoi(argv[1]);
sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(port); //本地字节序转成网络字节序、主机字节序转成网络字节序
saddr.sin_addr.s_addr = htonl(0); //任意的IP地址发过来的数据我们都接收
if(bind(sock, (sockaddr*)&saddr, sizeof(saddr)) != 0)
{
printf("bind port %d failed! \n", port);
return -2;
}
printf("bind port %d success!\n", port);
listen(sock, 10);
sockaddr_in cliAddr;
socklen_t socklen = sizeof(cliAddr);
//这个sock是专门用来建立连接的
//accept函数返回一个客户端的socket
int client = accept(sock, (sockaddr*)&cliAddr, &socklen);
printf("accept client %d\n", client);
char* ip = inet_ntoa(cliAddr.sin_addr);
unsigned short cliPort = ntohs(cliAddr.sin_port); //网络字节序转成本地字节序
printf("client ip is %s, port is %d\n", ip, cliPort);
char buf[1024] = {0};
for(;;)
{
int recvLen = recv(client, buf, sizeof(buf) - 1, 0);
if(recvLen <= 0)
break;
//赋一个\0
buf[recvLen] = '\0';
if(strstr(buf, "quit") != nullptr)
{
char re[] = "quit success!\n";
send(client, re, strlen(re) + 1, 0);
break;
}
int sendLen = send(client, "ok\n", 4, 0);
printf("recv %s\n", buf);
}
closesocket(client);
//closesocket(sock);
getchar();
return 0;
}
TCP的3次握手是由系统内部已经做完了,我们accept函数只是读取了它3次握手后的信息,所以说是在我们绑定bind、进行lisent之后,对方客户端用连接请求的时候,它就已经完成了3次握手。
//我们现在增加一个功能,每来一个客户端,我们就把它加到一个新的线程当中,这样保证我能同时并发处理多个客户端的数据,
//我们采用C++11的线程
//main.cpp
//
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#ifdef _WIN32
#include <windows.h>
#else
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#define closesocket close
#endif
#include <thread>
using namespace std;
class TcpThread
{
public:
void Main()
{
char buf[1024] = {0};
for(;;)
{
int recvLen = recv(client, buf, sizeof(buf) - 1, 0);
if(recvLen <= 0)
break;
//赋一个\0
buf[recvLen] = '\0';
if(strstr(buf, "quit") != nullptr)
{
char re[] = "quit success!\n";
send(client, re, strlen(re) + 1, 0);
break;
}
int sendLen = send(client, "ok\n", 4, 0);
printf("recv %s\n", buf);
}
closesocket(client);
}
int client = 0;
};
int main(int argc, char* argv[])
{
//在linux当中socket不需要初始化
#ifdef _WIN32
WSADATA ws;
WSAStartup(MAKEWORD(2, 2), &ws);
#endif
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock == -1)
{
printf("create socket failed! sock = %d\n", sock);
return -1;
}
printf("sock = [%d]\n", sock);
unsigned short port = 8080;
if(argc > 1)
port = atoi(argv[1]);
sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(port); //本地字节序转成网络字节序、主机字节序转成网络字节序
saddr.sin_addr.s_addr = htonl(0); //任意的IP地址发过来的数据我们都接收
if(bind(sock, (sockaddr*)&saddr, sizeof(saddr)) != 0)
{
printf("bind port %d failed! \n", port);
return -2;
}
printf("bind port %d success!\n", port);
listen(sock, 10);
for(;;)
{
sockaddr_in cliAddr;
socklen_t socklen = sizeof(cliAddr);
//这个sock是专门用来建立连接的
//accept函数返回一个客户端的socket
int client = accept(sock, (sockaddr*)&cliAddr, &socklen);
if(client <= 0)
break;
printf("accept client %d\n", client);
char* ip = inet_ntoa(cliAddr.sin_addr);
unsigned short cliPort = ntohs(cliAddr.sin_port); //网络字节序转成本地字节序
printf("client ip is %s, port is %d\n", ip, cliPort);
TcpThread *th = new TcpThread();
th->client = client;
thread subTh(&TcpThread::Main, th);
//释放我的主线程拥有的子线程的资源
sth.detach();
}
closesocket(client);
getchar();
return 0;
}
编译后报错,提示没有thread,并且你没有用c++11来进行编译,make只是一个简单的自动推导,它不会把这些参数加进去。
重写一个makefile:
tcpserver:main.cpp
g++ main.cpp -o tcpserver -std=c++11
编译报错,它是没找到pthread_create的定义,但是我们其实没调用这个函数,因为c++11它封装的这个thread里面调用了系统的pthread库,因为它针对windows和linux调用了不同的库,所以我们这里要把pthread库给它引用进来,这个库系统都是默认自带的。
我们再改下makefile:
tcpserver:main.cpp
g++ main.cpp -o tcpserver -std=c++11 -lpthread
这样就可以同时处理多路数据,我们就做了并发处理。
接下来我们在windows下编译该代码,正常情况下肯定会有一些错误,毕竟和linux有所区别。
//main.cpp
//
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#ifdef _WIN32
#include <windows.h>
#define socklen_t int
#else
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#define closesocket close
#endif
#include <thread>
using namespace std;
class TcpThread
{
public:
void Main()
{
char buf[1024] = {0};
for(;;)
{
int recvLen = recv(client, buf, sizeof(buf) - 1, 0);
if(recvLen <= 0)
break;
//赋一个\0
buf[recvLen] = '\0';
if(strstr(buf, "quit") != nullptr)
{
char re[] = "quit success!\n";
send(client, re, strlen(re) + 1, 0);
break;
}
int sendLen = send(client, "ok\n", 4, 0);
printf("recv %s\n", buf);
}
closesocket(client);
}
int client = 0;
};
int main(int argc, char* argv[])
{
//在linux当中socket不需要初始化
#ifdef _WIN32
WSADATA ws;
WSAStartup(MAKEWORD(2, 2), &ws);
#endif
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock == -1)
{
printf("create socket failed! sock = %d\n", sock);
return -1;
}
printf("sock = [%d]\n", sock);
unsigned short port = 8080;
if(argc > 1)
port = atoi(argv[1]);
sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(port); //本地字节序转成网络字节序、主机字节序转成网络字节序
saddr.sin_addr.s_addr = htonl(0); //任意的IP地址发过来的数据我们都接收
//为了不和c++11的bind冲突,这里我们用全局的bind
if(::bind(sock, (sockaddr*)&saddr, sizeof(saddr)) != 0)
{
printf("bind port %d failed! \n", port);
return -2;
}
printf("bind port %d success!\n", port);
listen(sock, 10);
for(;;)
{
sockaddr_in cliAddr;
socklen_t socklen = sizeof(cliAddr);
//这个sock是专门用来建立连接的
//accept函数返回一个客户端的socket
int client = accept(sock, (sockaddr*)&cliAddr, &socklen);
if(client <= 0)
break;
printf("accept client %d\n", client);
char* ip = inet_ntoa(cliAddr.sin_addr);
unsigned short cliPort = ntohs(cliAddr.sin_port); //网络字节序转成本地字节序
printf("client ip is %s, port is %d\n", ip, cliPort);
TcpThread *th = new TcpThread();
th->client = client;
thread subTh(&TcpThread::Main, th);
//释放我的主线程拥有的子线程的资源
sth.detach();
}
closesocket(client);
getchar();
return 0;
}
第三章 TCP类封装
添加一个类,XTcp,勾选虚析构函数,确定。
大家记得一点,你这个头文件的引用尽量要放在.cpp当中,为什么要放在.cpp当中,而不放在.h当中呢?
你.h如果是被第三方引用,对于调用者来说他只要知道.h就行了,他不需要知道.cpp,你甚至可以把.cpp变成动态链接库,但是.h你是需要给他的,如果你.h当中引用了一些头文件比如windows.h,那就有可能给调用者造成冲突影响,就像std命名空间里面有很多库会与很多库产生冲突,包括windows.h也是一样,它涉及到引用次序,也会对很多函数产生影响;
所以说我们最保险的做法,像windows.h一定不要放在.h当中,放在.cpp当中这样就不会对别人产生影响;产生影响只局限于你本.cpp内部,你可以内部就把它调整过来,当出现一种交错影响的时候,那代码是相当的难调试的。
//XTcp.h
//
#pragma once
#include <string>
class XTcp
{
public:
int CreateSocket();
//绑定端口和listen
bool Bind(unsigned short port);
XTcp Accept();
void Close();
int Recv(char *buf, int bufsize);
int Send(const char *buf, int sendsize);
XTcp();
virtual ~XTcp();
int sock = 0;
unsigned short port = 0;
std::string ip;
};
//XTcp.cpp
//
#include "XTcp.h"
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#ifdef _WIN32
#include <windows.h>
#define socklen_t int
#else
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#define closesocket close
#endif
#include <thread>
using namespace std;
XTcp::XTcp()
{
#ifdef _WIN32
static bool first = true;
if(first)
{
first = false;
WSADATA ws;
WSAStartup(MAKEWORD(2, 2), &ws);
}
#endif
}
int XTcp::CreateSocket()
{
if(sock <= 0)
{
sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock == -1)
{
printf("create socket failed! sock = %d\n", sock);
}
}
return sock;
}
bool XTcp::Bind(unsigned short port)
{
if(sock <= 0)
{
CreateSocket();
}
sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(port); //本地字节序转成网络字节序、主机字节序转成网络字节序
saddr.sin_addr.s_addr = htonl(0); //任意的IP地址发过来的数据我们都接收
//为了不和c++11的bind冲突,这里我们用全局的bind
if(::bind(sock, (sockaddr*)&saddr, sizeof(saddr)) != 0)
{
printf("bind port %d failed! \n", port);
return false;
}
printf("bind port %d success!\n", port);
listen(sock, 10);
return true;
}
XTcp XTcp::Accept()
{
XTcp tcp;
sockaddr_in cliAddr;
socklen_t socklen = sizeof(cliAddr);
//这个sock是专门用来建立连接的
//accept函数返回一个客户端的socket
int client = accept(sock, (sockaddr*)&cliAddr, &socklen);
if(client <= 0)
return tcp;
printf("accept client %d\n", client);
tcp.sock = client;
tcp.ip = inet_ntoa(cliAddr.sin_addr);
tcp.port = ntohs(cliAddr.sin_port); //网络字节序转成本地字节序
printf("client ip is %s, port is %d\n", tcp.ip.c_str(), tcp.port);
return tcp;
}
int XTcp::Recv(char *buf, int bufsize)
{
return recv(sock, buf, bufsize, 0);
}
int XTcp::Send(const char *buf, int size)
{
int sendedSize = 0;
while(sendedSize != size)
{
int len = send(sock, buf + sendedSize, sendsize - sendedSize, 0);
if(len <= 0)
break;
sendedSize += len;
}
return sendedSize;
}
void XTcp::Close()
{
if(sock <= 0) return;
closesocket(sock);
}
XTcp::~XTcp()
{
}
//main.cpp
//
#include <stdlib.h>
#include "XTcp.h"
#include <thread>
#include <string.h>
class TcpThread
{
public:
void Main()
{
char buf[1024] = {0};
for(;;)
{
int recvLen = client.Recv(buf, sizeof(buf) - 1);
if(recvLen <= 0)
break;
//赋一个\0
buf[recvLen] = '\0';
if(strstr(buf, "quit") != nullptr)
{
char re[] = "quit success!\n";
client.Send(re, strlen(re) + 1);
break;
}
int sendLen = client.Send("ok\n", 4);
printf("recv %s\n", buf);
}
client.Close();
}
XTcp client;
};
int main(int argc, char* argv[])
{
unsigned short port = 8080;
if(argc > 1)
{
port = atoi(argv[1]);
}
XTcp server;
server.CreateSocket();
server.Bind(port);
for(;;)
{
XTcp client = server.Accept();
TcpThread *th = new TcpThread();
th->client = client;
std::thread subTh(&TcpThread::Main, th);
//释放我的主线程拥有的子线程的资源
sth.detach();
}
server.Close();
getchar();
return 0;
}
在windows下编译运行测试成功!
在linux下编译报错:
错误提示Recv、Send函数和Close函数没有定义,那是因为我们的makefile里面没有包含XTcp.cpp这个文件,我们把makefile修改一下:
tcpserver:main.cpp XTcp.cpp XTcp.h
g++ main.cpp XTcp.cpp -o tcpserver -std=c++11 -lpthread
创建XTcp动态dll链接库项目并测试
我们把前面做的XTcp这个类放到一个动态链接库当中,分别在windows和linux当中都来实现这个动态链接库。
我们新建一个Win32 控制台应用程序,XSocket,应用程序类型选择DLL,附加选项选择导出符号,导出符号的好处项目当中有一些宏已经帮你定义好了,省得你定义了。
把前面做的XTcp.h和XTcp.cpp拷贝到当前项目目录中,并添加到当前项目中。
//XTcp.h
//
#pragma once
#include <string>
#ifdef XSOCKET_EXPORTS
#define XSOCKET_API __declspec(dllexport)
#else
#define XSOCKET_API __declspec(dllimport)
#endif
class XSOCKET_EXPORTS XTcp
{
public:
int CreateSocket();
//绑定端口和listen
bool Bind(unsigned short port);
XTcp Accept();
void Close();
int Recv(char *buf, int bufsize);
int Send(const char *buf, int sendsize);
XTcp();
virtual ~XTcp();
int sock = 0;
unsigned short port = 0;
char ip[16];
};
记得关闭预编译头。
//XTcp.cpp
//
#include "XTcp.h"
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#ifdef _WIN32
#include <windows.h>
#define socklen_t int
#else
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#define closesocket close
#endif
#include <thread>
using namespace std;
XTcp::XTcp()
{
#ifdef _WIN32
static bool first = true;
if(first)
{
first = false;
WSADATA ws;
WSAStartup(MAKEWORD(2, 2), &ws);
}
#endif
}
int XTcp::CreateSocket()
{
if(sock <= 0)
{
sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock == -1)
{
printf("create socket failed! sock = %d\n", sock);
}
}
return sock;
}
bool XTcp::Bind(unsigned short port)
{
if(sock <= 0)
{
CreateSocket();
}
sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(port); //本地字节序转成网络字节序、主机字节序转成网络字节序
saddr.sin_addr.s_addr = htonl(0); //任意的IP地址发过来的数据我们都接收
//为了不和c++11的bind冲突,这里我们用全局的bind
if(::bind(sock, (sockaddr*)&saddr, sizeof(saddr)) != 0)
{
printf("bind port %d failed! \n", port);
return false;
}
printf("bind port %d success!\n", port);
listen(sock, 10);
return true;
}
XTcp XTcp::Accept()
{
XTcp tcp;
sockaddr_in cliAddr;
socklen_t socklen = sizeof(cliAddr);
//这个sock是专门用来建立连接的
//accept函数返回一个客户端的socket
int client = accept(sock, (sockaddr*)&cliAddr, &socklen);
if(client <= 0)
return tcp;
printf("accept client %d\n", client);
tcp.sock = client;
char *ip = inet_ntoa(cliAddr.sin_addr);
strcpy(tcp.ip, ip);
tcp.port = ntohs(cliAddr.sin_port); //网络字节序转成本地字节序
printf("client ip is %s, port is %d\n", tcp.ip, tcp.port);
return tcp;
}
int XTcp::Recv(char *buf, int bufsize)
{
return recv(sock, buf, bufsize, 0);
}
int XTcp::Send(const char *buf, int size)
{
int sendedSize = 0;
while(sendedSize != size)
{
int len = send(sock, buf + sendedSize, sendsize - sendedSize, 0);
if(len <= 0)
break;
sendedSize += len;
}
return sendedSize;
}
void XTcp::Close()
{
if(sock <= 0) return;
closesocket(sock);
}
XTcp::~XTcp()
{
}
我们参考开源项目,调整一下代码的结构,创建几个目录:bin、src、lib、doc。
- 打开项目属性、配置属性、常规,把项目的输出目录修改为
..\..\bin\
- 链接器、高级,我们修改导入库,把生成的lib文件放到
..\..\lib\$(TargetName).lib
- 配置属性、调试,修改工作目录为
..\..\bin\
,调试中的这个工作目录是指你这个执行程序在哪个路径下执行,因为你的dll在bin目录中,所以你这个调试路径不设置为bin目录的话,你就无法通过测试程序调试这个dll。
测试
右击解决方案,添加新项目,tcpserver,应用程序类型选择控制台应用程序,空项目。
打开解决方案属性,设置启动项目为tcpserver,项目依赖项为tcpserver依赖于xsocket,也就是当tcpserver要运行的时候,必须保证xsocket已经编译过了。
给tcpserver项目添加server.cpp,我们还要引用头文件XTcp.h和对应的链接库:
- 打开tcpserver属性,C/C++,设置附加包含目录为
..\xsocket
- 链接器,常规,设置附加库目录为
..\..\lib
- 链接器,输入,给附加依赖项添加
xsocket.lib
//server.cpp
//
#include "XTcp.h"
int main(int argc, char *argv[])
{
unsigned short port = 8080;
if(argc > 1)
{
port = atoi(argv[1]);
}
XTcp server;
server.Bind(port);
XTcp client = server.Accept();
server.Close();
getchar();
return 0;
}
我们先写一个简单的,保证输出测试成功。
打开项目的配置属性,常规,设置输出目录为..\..\bin\
配置属性,调试,设置工作目录为..\..\bin\
测试成功后,我们再把之前代码中的线程那部分代码拷贝过来:
//server.cpp
//
#include <stdlib.h>
#include "XTcp.h"
#include <thread>
#include <string.h>
class TcpThread
{
public:
void Main()
{
char buf[1024] = {0};
for(;;)
{
int recvLen = client.Recv(buf, sizeof(buf) - 1);
if(recvLen <= 0)
break;
//赋一个\0
buf[recvLen] = '\0';
if(strstr(buf, "quit") != nullptr)
{
char re[] = "quit success!\n";
client.Send(re, strlen(re) + 1);
break;
}
int sendLen = client.Send("ok\n", 4);
printf("recv %s\n", buf);
}
client.Close();
}
XTcp client;
};
int main(int argc, char* argv[])
{
unsigned short port = 8080;
if(argc > 1)
{
port = atoi(argv[1]);
}
XTcp server;
server.CreateSocket();
server.Bind(port);
for(;;)
{
XTcp client = server.Accept();
TcpThread *th = new TcpThread();
th->client = client;
std::thread subTh(&TcpThread::Main, th);
//释放我的主线程拥有的子线程的资源
sth.detach();
}
server.Close();
getchar();
return 0;
}
创建XTcp动态so链接库项目(Linux)并测试
在xsocket项目目录中,新建并编写makefile文件:
libxsocket.so:XTcp.cpp XTcp.h
g++ $+ -o $@ -fpic -shared -std=c++11
编译出错,主要是在linux当中不认识windows中的几个宏,我们修改动态链接库的代码:
//XTcp.h
//
#ifndef _XTCP_H_
#define _XTCP_H_
#include <string>
#ifdef _WIN32
#ifdef XSOCKET_EXPORTS
#define XSOCKET_API __declspec(dllexport)
#else
#define XSOCKET_API __declspec(dllimport)
#endif
#else
#define XSOCKET_API
#endif
class XSOCKET_EXPORTS XTcp
{
public:
int CreateSocket();
//绑定端口和listen
bool Bind(unsigned short port);
XTcp Accept();
void Close();
int Recv(char *buf, int bufsize);
int Send(const char *buf, int sendsize);
XTcp();
virtual ~XTcp();
int sock = 0;
unsigned short port = 0;
char ip[16];
};
#endif
//XTcp.cpp
//
#include "XTcp.h"
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#ifdef _WIN32
#include <windows.h>
#define socklen_t int
#else
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#define closesocket close
#endif
#include <thread>
using namespace std;
XTcp::XTcp()
{
#ifdef _WIN32
static bool first = true;
if(first)
{
first = false;
WSADATA ws;
WSAStartup(MAKEWORD(2, 2), &ws);
}
#endif
}
int XTcp::CreateSocket()
{
if(sock <= 0)
{
sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock == -1)
{
printf("create socket failed! sock = %d\n", sock);
}
}
return sock;
}
bool XTcp::Bind(unsigned short port)
{
if(sock <= 0)
{
CreateSocket();
}
sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(port); //本地字节序转成网络字节序、主机字节序转成网络字节序
saddr.sin_addr.s_addr = htonl(0); //任意的IP地址发过来的数据我们都接收
//为了不和c++11的bind冲突,这里我们用全局的bind
if(::bind(sock, (sockaddr*)&saddr, sizeof(saddr)) != 0)
{
printf("bind port %d failed! \n", port);
return false;
}
printf("bind port %d success!\n", port);
listen(sock, 10);
return true;
}
XTcp XTcp::Accept()
{
XTcp tcp;
sockaddr_in cliAddr;
socklen_t socklen = sizeof(cliAddr);
//这个sock是专门用来建立连接的
//accept函数返回一个客户端的socket
int client = accept(sock, (sockaddr*)&cliAddr, &socklen);
if(client <= 0)
return tcp;
printf("accept client %d\n", client);
tcp.sock = client;
char *ip = inet_ntoa(cliAddr.sin_addr);
strcpy(tcp.ip, ip);
tcp.port = ntohs(cliAddr.sin_port); //网络字节序转成本地字节序
printf("client ip is %s, port is %d\n", tcp.ip, tcp.port);
return tcp;
}
int XTcp::Recv(char *buf, int bufsize)
{
return recv(sock, buf, bufsize, 0);
}
int XTcp::Send(const char *buf, int size)
{
int sendedSize = 0;
while(sendedSize != size)
{
int len = send(sock, buf + sendedSize, sendsize - sendedSize, 0);
if(len <= 0)
break;
sendedSize += len;
}
return sendedSize;
}
void XTcp::Close()
{
if(sock <= 0) return;
closesocket(sock);
}
XTcp::~XTcp()
{
}
测试
在tcpserver项目目录中,新建并编写makefile文件:
tcpserver:server.cpp
g++ $+ -o $@
编译报错:
我们要把头文件路径给它指定一下:
tcpserver:server.cpp
g++ $+ -o $@ -I../xsocket/ -std=c++11 -lpthread -lxsocket -L../xsocket
提示说它找不到这个xsocket这个库,在linux你必须要指定环境变量,所以我们一个方案就是可以写一个脚本run:
export LD_LIBRARY_PATH = ../xsocket
./tcpserver
chmod +x run
./run
还有一个省事的方案,你把动态链接库每次改变的时候,你把它拷贝到系统目录下面:
libxsocket.so:XTcp.cpp XTcp.h
g++ $+ -o $@ -fpic -shared -std=c++11
cp *.so /usr/lib/
假设我们想直接输入tcpserver运行呢?
那我们要把tcpserver也放在系统的环境变量下面去。
tcpserver:server.cpp
g++ $+ -o $@ -I../xsocket/ -std=c++11 -lpthread -lxsocket -L../xsocket
cp $@ /usr/bin/
第四章 TCP客户端
新建一个控制台应用程序的空项目,在这个项目当中我们引用之前做好的XTcp这个库,右击解决方案,添加现有项目,把我们的xsocket项目加进来。
再做一下启动项和依赖项的设置。
再把输出目录,工作目录,附加包含目录,附加库目录,附加依赖项。
我们这个客户端很简单,就演示一下建立连接、发送数据、接收数据。
//tcpclient.cpp
//
#include "XTcp.h"
int main()
{
XTcp client;
return 0;
}
看上图,我们来看一下TCP的三次握手的过程。
服务端这里创建了一个Socket,绑定了一个端口,通过listen开始监听,listen之后就是被动打开,也就是说它是由对方发送信息,它被动的反应,而不是主动去做。
服务端做好了之后,客户端是怎么进行三次握手的呢?
客户端它也要创建一个Socket,通过这个Socket调用Connect,Connect也是一个阻塞的函数,它是主动打开,它发一个SYN一个J过去,服务端收到这个包之后,服务端也会发一个SYN一个K过去,然后它会回一个ack的J+1回去,这个过程你在TCP协议头当中都会包含这几个内容;
当客户端收到服务端发过来的这个回应之后,Connect就会再发送一个ack K+1过去,就是告诉服务器我也收到你的信息了。
三次握手的过程在Connect函数内部其实就已经完成了,Accept只是读三次握手已经完成后的数据。
//XTcp.h
//
#ifndef _XTCP_H_
#define _XTCP_H_
#include <string>
#ifdef _WIN32
#ifdef XSOCKET_EXPORTS
#define XSOCKET_API __declspec(dllexport)
#else
#define XSOCKET_API __declspec(dllimport)
#endif
#else
#define XSOCKET_API
#endif
class XSOCKET_EXPORTS XTcp
{
public:
int CreateSocket();
//绑定端口和listen
bool Bind(unsigned short port);
XTcp Accept();
void Close();
int Recv(char *buf, int bufsize);
int Send(const char *buf, int sendsize);
bool Connect(const char *ip, unsigned short port);
XTcp();
virtual ~XTcp();
int sock = 0;
unsigned short port = 0;
char ip[16];
};
#endif
//XTcp.cpp
//
#include "XTcp.h"
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#ifdef _WIN32
#include <windows.h>
#define socklen_t int
#else
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#define closesocket close
#endif
#include <thread>
using namespace std;
XTcp::XTcp()
{
#ifdef _WIN32
static bool first = true;
if(first)
{
first = false;
WSADATA ws;
WSAStartup(MAKEWORD(2, 2), &ws);
}
#endif
}
int XTcp::CreateSocket()
{
if(sock <= 0)
{
sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock == -1)
{
printf("create socket failed! sock = %d\n", sock);
}
}
return sock;
}
bool XTcp::Bind(unsigned short port)
{
if(sock <= 0)
{
CreateSocket();
}
sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(port); //本地字节序转成网络字节序、主机字节序转成网络字节序
saddr.sin_addr.s_addr = htonl(0); //任意的IP地址发过来的数据我们都接收
//为了不和c++11的bind冲突,这里我们用全局的bind
if(::bind(sock, (sockaddr*)&saddr, sizeof(saddr)) != 0)
{
printf("bind port %d failed! \n", port);
return false;
}
printf("bind port %d success!\n", port);
listen(sock, 10);
return true;
}
XTcp XTcp::Accept()
{
XTcp tcp;
sockaddr_in cliAddr;
socklen_t socklen = sizeof(cliAddr);
//这个sock是专门用来建立连接的
//accept函数返回一个客户端的socket
int client = accept(sock, (sockaddr*)&cliAddr, &socklen);
if(client <= 0)
return tcp;
printf("accept client %d\n", client);
tcp.sock = client;
char *ip = inet_ntoa(cliAddr.sin_addr);
strcpy(tcp.ip, ip);
tcp.port = ntohs(cliAddr.sin_port); //网络字节序转成本地字节序
printf("client ip is %s, port is %d\n", tcp.ip, tcp.port);
return tcp;
}
int XTcp::Recv(char *buf, int bufsize)
{
return recv(sock, buf, bufsize, 0);
}
int XTcp::Send(const char *buf, int size)
{
int sendedSize = 0;
while(sendedSize != size)
{
int len = send(sock, buf + sendedSize, sendsize - sendedSize, 0);
if(len <= 0)
break;
sendedSize += len;
}
return sendedSize;
}
void XTcp::Close()
{
if(sock <= 0) return;
closesocket(sock);
}
bool XTcp::Connect(const char *ip, unsigned short port)
{
if(sock <= 0)
CreateSocket();
sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(port);
saddr.sin_addr.s_addr = inet_addr(ip);
if(connect(sock, (sockaddr*)&saddr, sizeof(saddr)) != 0)
{
printf("connect %s:%d failed!:%s\n", ip, port, strerror(errno));
return false;
}
printf("connect %s:%d success!\n", ip, port);
return true;
}
XTcp::~XTcp()
{
}
//tcpclient.cpp
//
#include "XTcp.h"
int main()
{
XTcp client;
client.Connect("192.168.0.108", 8080);
client.Send("client", 6);
char buf[1024] = {0};
client.Recv(buf, sizeof(buf));
printf("client recv : %s\n", buf);
return 0;
}
tcpclient:tcpclient.cpp
g++ $+ -o $@ -I../xsocket/ -std=c++11 -lpthread -lxsocket -L../xsocket
cp $@ /usr/bin/