前言
因为编程需要,需要将tcp服务增加到软件中进行本地测试,所以开始学习socket编程,首先需要了解socket相关的知识。本文只摘录编程中最先需要了解的内容,详细请参考以下两个博客: https://blog.csdn.net/xiaoquantouer/article/details/58001960 https://www.cnblogs.com/helloworldcode/p/10610581.html sockets(套接字)编程有三种,流式套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM),原始套接字(SOCK_RAW);基于TCP的socket编程是采用的流式套接字。
编程模式
客户端/服务端模式:
在TCP/IP网络应用中,通信的两个进程相互作用的主要模式是客户/服务器模式,即客户端向服务器发出请求,服务器接收请求后,提供相应的服务。客户/服务器模式的建立基于以下两点:
(1)建立网络的起因是网络中软硬件资源、运算能力和信息不均等,需要共享,从而就让拥有众多资源的主机提供服务,资源较少的客户请求服务这一非对等作用。
(2)网间进程通信完全是异步的,相互通信的进程间既不存在父子关系,又不共享内存缓冲区。
因此需要一种机制为希望通信的进程间建立联系,为二者的数据交换提供同步,这就是基于客户/服务端模式的TCP/IP。
服务端:建立socket,声明自身的端口号和地址并绑定到socket,使用listen打开监听,然后不断用accept去查看是否有连接,如果有,捕获socket,并通过recv获取消息的内容,通信完成后调用closeSocket关闭这个对应accept到的socket,如果不再需要等待任何客户端连接,那么用closeSocket关闭掉自身的socket。
客户端:建立socket,通过端口号和地址确定目标服务器,使用Connect连接到服务器,send发送消息,等待处理,通信完成后调用closeSocket关闭socket。
编程步骤
(1)服务端
1、加载套接字库,创建套接字(WSAStartup()/socket());
2、绑定套接字到一个IP地址和一个端口上(bind());
3、将套接字设置为监听模式等待连接请求(listen());
4、请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept());
5、用返回的套接字和客户端进行通信(send()/recv());
6、返回,等待另一个连接请求;
7、关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup());
(2)客户端
1、加载套接字库,创建套接字(WSAStartup()/socket());
2、向服务器发出连接请求(connect());
3、和服务器进行通信(send()/recv());
4、关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup());
一对一的TCP服务
讲解socket编程的博客和代码并不少,我也参考了不少的博客,最终选择了https://www.cnblogs.com/zhangaihua/p/3718089.html,因为其中的注释较多,在服务器端和客户端代码的重要步骤做了解释,方便理解;此外其差错控制也相当细,当TCP服务出现问题时,可以及时获取到错误问题,以下是其修改格式的代码:
Server.cpp
#include<WINSOCK2.H>
#include<iostream>
using namespace std;
#include<stdlib.h>
#define BUF_SIZE 64
#pragma comment(lib,"WS2_32.lib")
int main()
{
WSADATA wsd;
SOCKET sServer;
SOCKET sClient;
int retVal;
char buf[BUF_SIZE];
if(WSAStartup(MAKEWORD(2,2),&wsd)!=0){
printf("WSAStartup failed!\n");
return 1;
}
sServer=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(INVALID_SOCKET==sServer){
printf("socket failed!\n");
WSACleanup();
return -1;
}
SOCKADDR_IN addrServ;
addrServ.sin_family=AF_INET;
addrServ.sin_port=htons(1298);
addrServ.sin_addr.S_un.S_addr=htonl(INADDR_ANY);
retVal=bind(sServer,(const struct sockaddr*)&addrServ,sizeof(SOCKADDR_IN));
if(SOCKET_ERROR==retVal){
printf("bind failed!\n");
closesocket(sServer);
WSACleanup();
return -1;
}
retVal=listen(sServer,1);
if(SOCKET_ERROR==retVal){
printf("listen failed!\n");
closesocket(sServer);
WSACleanup();
return -1;
}
printf("TCPServer start...\n");
sockaddr_in addrClient;
int addrClientlen=sizeof(addrClient);
sClient=accept(sServer,(sockaddr FAR*)&addrClient,&addrClientlen);
if(INVALID_SOCKET==sClient){
printf("accept failed!\n");
closesocket(sServer);
WSACleanup();
return -1;
}
while(true){
ZeroMemory(buf,BUF_SIZE);
retVal=recv(sClient,buf,BUFSIZ,0);
if(SOCKET_ERROR==retVal){
printf("recv failed!\n");
closesocket(sServer);
closesocket(sClient);
WSACleanup();
return -1;
}
SYSTEMTIME st;
GetLocalTime(&st);
char sDateTime[30];
sprintf(sDateTime,"%4d-%2d-%2d-%2d:%2d:%2d:%2d",st.wYear,st.wMonth,st.wDay,st.wHour,st.wMinute,st.wSecond);
printf("%s,Recv From Client [%s:%d]:%s\n",sDateTime,inet_ntoa(addrClient.sin_addr),addrClient.sin_port,buf);
if(strcmp(buf,"quit")==0){
retVal=send(sClient,"quit",strlen("quit"),0);
break;
}
else{
char msg[BUF_SIZE];
sprintf(msg,"Message received - %s\n",buf);
retVal=send(sClient,msg,strlen(msg),0);
if(SOCKET_ERROR==retVal){
printf("send failed!\n");
closesocket(sServer);
closesocket(sClient);
WSACleanup();
return -1;
}
}
}
closesocket(sServer);
closesocket(sClient);
WSACleanup();
system("pause");
return 0;
}
Client.cpp
#include<WINSOCK2.H>
#include<iostream>
#include<string>
using namespace std;
#include<stdlib.h>
#define BUF_SIZE 64
#pragma comment(lib,"WS2_32.lib")
int main()
{
WSADATA wsd;
SOCKET sHost;
SOCKADDR_IN servAddr;
int retVal;
char buf[BUF_SIZE];
if(WSAStartup(MAKEWORD(2,2),&wsd)!=0){
printf("WSAStartup failed!\n");
return 1;
}
sHost=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(INVALID_SOCKET==sHost){
printf("socket failed!\n");
WSACleanup();
return -1;
}
servAddr.sin_family=AF_INET;
servAddr.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");
servAddr.sin_port=htons(1298);
int sServerAddlen=sizeof(servAddr);
retVal=connect(sHost,(LPSOCKADDR)&servAddr,sizeof(servAddr));
if(SOCKET_ERROR==retVal){
printf("send failed!\n");
closesocket(sHost);
WSACleanup();
return -1;
}
while(true){
printf("input a string to send:");
std::string str;
std::getline(std::cin,str);
ZeroMemory(buf,BUF_SIZE);
strcpy(buf,str.c_str());
retVal=send(sHost,buf,strlen(buf),0);
if(SOCKET_ERROR==retVal){
printf("send failed!\n");
closesocket(sHost);
WSACleanup();
return -1;
}
retVal=recv(sHost,buf,sizeof(buf)+1,0);
printf("Recv From Server : %s\n",buf);
if(strcmp(buf,"quit")==0){
printf("quit!\n");
break;
}
}
return 0;
}
我是在DEV C++环境跑的程序,可能会遇到问题:undefined reference to ‘_imp_WSAStartup’
解决方法:
工具->编译选项->在连接器命令行加入如下命令里面添加-lwsock32 即可 然后devc++重新构建运行程序问题解决。
一对多的TCP服务
要想实现一对多的TCP服务,开通不同的端口用于一个client连接是不现实的,而且服务端的端口,能够接收多个用户连接的,但是每次都需要连接服务端的套接字,而且accep函数是阻塞函数(只有当有用户连接时才能唤醒),同样是阻塞函数的还有接收数据的函数recv(只有当有数据接收到才能唤醒)。那我们就可以在服务端的第四步进行循环监听连接请求,并每次连接上后开辟一个线程进行tcp收发数据服务。 原先的Client.cpp并不需要额外的改动,Server.cpp要进行较多的改动。改动的地方主要有以下:
- listen的限制数量增多
- 其中一个Client连接断开时,不能将Server释放,因为可能有其他的Client还在连接或者有其他Client将要连接
- 开辟的线程中可能需要主线程的参数,传入的参数可能需要用结构体包装
除此之外,一般来说Tcp服务都是部署到后台默默地进行服务,只把连接到Client的交互开辟线程还远远不够,因为accept也是个阻塞函数,也可以放到一个线程里。以下是实现代码
M_Server.cpp
#include<WINSOCK2.H>
#include<iostream>
using namespace std;
#include<stdlib.h>
#define BUF_SIZE 64
#define QUEUE_SIZE 5
#pragma comment(lib,"WS2_32.lib")
struct Param {
SOCKET sServer;
SOCKET sClient;
sockaddr_in addrClient;
};
DWORD WINAPI mainThread(LPVOID lpargs);
DWORD WINAPI ThreadProcServerConmunicate(LPVOID lpParameter);
int main()
{
int port=1298;
CreateThread(NULL,0,mainThread,&port,0,NULL);
Sleep(100000);
return 0;
}
DWORD WINAPI mainThread(LPVOID lpargs)
{
int *p = (int *)lpargs;
int port = *p;
WSADATA wsd;
SOCKET sServer;
SOCKET sClient;
int retVal;
char buf[BUF_SIZE];
if(WSAStartup(MAKEWORD(2,2),&wsd)!=0)
{
printf("init failed!\n");
return 1;
}
sServer=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(INVALID_SOCKET==sServer)
{
printf("socket failed!\n");
WSACleanup();
return -1;
}
SOCKADDR_IN addrServ;
addrServ.sin_family=AF_INET;
addrServ.sin_port=htons(port);
addrServ.sin_addr.S_un.S_addr=htonl(INADDR_ANY);
retVal=bind(sServer,(const struct sockaddr*)&addrServ,sizeof(SOCKADDR_IN));
if(SOCKET_ERROR==retVal)
{
printf("bind failed!\n");
closesocket(sServer);
WSACleanup();
return -1;
}
retVal=listen(sServer,QUEUE_SIZE);
if(SOCKET_ERROR==retVal)
{
printf("listen failed!\n");
closesocket(sServer);
WSACleanup();
return -1;
}
printf("TCPServer start...\n");
while(true)
{
sockaddr_in addrClient;
int addrClientlen=sizeof(addrClient);
sClient=accept(sServer,(sockaddr FAR*)&addrClient,&addrClientlen);
if(INVALID_SOCKET==sClient)
{
printf("accept failed!\n");
WSACleanup();
system("pause");
return -1;
}
else{
printf("一个客户端已经连接到本机的%d端口,SOCKET是 : %u \n",port, sServer);
Param param;
param.sServer=sServer;
param.sClient=sClient;
param.addrClient=addrClient;
CreateThread(
NULL,
0,
ThreadProcServerConmunicate,
¶m,
0,
NULL
);
}
}
closesocket(sServer);
closesocket(sClient);
WSACleanup();
system("pause");
return 0;
}
DWORD WINAPI ThreadProcServerConmunicate(LPVOID lpParameter)
{
int iPid = GetCurrentThreadId();
cout<<"The process id is: "<<iPid<<endl;
Param *param = (Param *)lpParameter;
SOCKET sServer =param->sServer;
SOCKET sClient =param->sClient;
sockaddr_in addrClient =param->addrClient;
int retVal = 0;
while (1)
{
char buf[BUF_SIZE];
retVal=recv(sClient,buf,BUF_SIZE,0);
if(SOCKET_ERROR==retVal)
{
printf("recv failed!\n");
closesocket(sClient);
return -1;
}
SYSTEMTIME st;
GetLocalTime(&st);
char sDateTime[30];
sprintf(sDateTime,"%4d-%02d-%02d %2d:%2d:%2d",st.wYear,st.wMonth,st.wDay,st.wHour,st.wMinute,st.wSecond);
printf("%s,Recv From Client [%s:%d]:%s\n",sDateTime,inet_ntoa(addrClient.sin_addr),addrClient.sin_port,buf);
if(strcmp(buf,"quit")==0)
{
retVal=send(sClient,"quit",strlen("quit"),0);
break;
}
else
{
char msg[BUF_SIZE];
sprintf(msg,"Message received - %s\n",buf);
retVal=send(sClient,msg,strlen(msg),0);
if(SOCKET_ERROR==retVal)
{
printf("send failed!\n");
closesocket(sClient);
return -1;
}
}
}
pthread_detach(pthread_self());
return 0;
}
最终效果:
|