c++使用socket编程小demo

注意:此代码是在window平台下;linux需要自己更改部分代码,本文尽可能的使用c++标准所支持的规范来编写;

如果setsockopt返回-1且errno为22,则可能是setsockopt(s, SOL_SOCKET, SO_BROADCAST, char*, int), 你可以使用int,bool等等类型强转来达到目的,但是这样可能会出现返回-1且errno为22这个问题

一、简单介绍socket编程所需要使用的函数

1:socket函数

int sock(int af, int type, int protocol);
/*
 * 作用:创建一个socket
 *
 * 参数:
 * af:  (address family)常见的一般有AF_INET、AF_INET6、AF_UNIX
 *         AF_INET : intetnetwork: UDP,TCP, etc
 *         AF_INET6: Internetwork Version 6
 *         AF_UNIX : local to host(pipes, portals)
 *
 * type: 常见的一般有SOCK_STREAM、SOCK_DGRAM
 *         SOCK_STREAM:stream socket
 *         SOCK_DGRAM :datadgram socket
 *
 * protocol:常见的一般有IPPROTO_TCP、IPPROTO_UDP等
 *         IPPROTO_TCP:tcp协议
 *         IPPROTO_UDP:udp协议
 *
 * return:
 * ~0:创建失败,其他成功,(~是取反运算符,即0按位取反)
 */

2:bind函数

int bind(int s, const struct sockaddr * addr, int addrLen);
/*
 * 作用:将套接字s绑定到指定地址addr
 *
 * s:传入的套接字,由socket函数得到
 *
 * addr:地址变量,包含family、port、ip_addr等信息,这里只是说有这些信息,不代表有这些成员
 * 
 * addrLen:addr所占用的字节数
 *
 * return:
 *     -1表示失败,0表示成功,至于其他的我也不清楚,可以自行百度
 */

3:listen函数

int listen(int s, int backlog);
/*
 * 作用:listen for connections on a socket(来自linux的man命令)
 *
 * s:套接字,由socket函数得到
 *
 * backlog:存储队列长度
 *
 * return:
 *     0表示成功,-1表示失败,其他的不知道
 */

4:connect函数

int connect(int s, const struct sockaddr *addr, int addrLen);
/*
 * 作用,尝试连接addr指定的服务器
 *
 * s:套接字,有socket得到
 *
 * addr:目标地址,一般包括family、port、ip_addr等信息,这里只是说有这些信息,不代表有这些成员
 *
 * addrLen:addr所占用的字节数
 *
 * return:
 *     0成功,-1失败
 */

5:accept函数

int accept(int s, struct sockaddr *addr, int *addrLen);
/*
 * 作用:accept a connection on a socket(来自linux的man命令)(man accept4)
 *
 * s:套接字,由socket函数得到
 *
 * addr:地址,发起连接的addr
 *
 * addrLen:是一个value-result参数,必须为addr占用的字节数,但函数执行完之后可能会改变
 *
 * return:
 *     -1表示失败,其他的:和发起连接的客户进行通信的套接字,也就是说和这个连接通信的话,可以使用这
 * 个套接字
 */

6:send函数

int send(int s, const char *buff, int size, int flags);
/*
 * 作用:发送数据
 *
 * s:套接字
 *
 * buff:数据地址
 *
 * size:发送数据的大小
 *
 * flags:the flahs argument is the bitwise OR of zero or more of the fllowing flags ...
 *        有点多,自己查,我也不太懂,有会的可以教教我
 *
 * return:
 *     实际发送的数据大小
 */

7:recv函数

int recv(int s, char *buff, int size, int flags);
/*
 * 作用:接收数据
 *
 * s:套接字
 *
 * buff:数据地址
 *
 * size:最大接收
 *
 * flags:the flahs argument is formed by ORing one or more of the following values ...
 *        有点多,自己查,我也不太懂,有会的可以教教我
 *
 * return:
 *     实际接收到的数据大小
 */

二、demo

2.1:server.cpp

#include <iostream>
#include <string>
#include <thread>
#include <vector>
#include <map>

/*window下面socket编程需要的头文件和库*/
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")

//简单的封装一下标准库的thread
//就多了一个isFinished函数,用来判断当前thread是否执行完毕
class CThread
{
public:
    explicit CThread() : m_thread(), m_isFinished(false) {}
    ~CThread() { if(m_thread.joinable()) m_thread.join(); }

    CThread(const CThread*) = delete;
    CThread &operator=(const CThread&) = delete;

    CThread(CThread &&thread) : m_thread(std::move(thread.m_thread)), m_isFinished(thread.m_isFinished) {}

    template<typename Fn, typename ... Args>
    void run(Fn f, Args ... args)
    {
        m_thread = std::thread([this](Fn fn, Args ... args) {
            fn(args ...);
            m_isFinished = true;
        }, f, args ...);
    }

    void join() { m_thread.join(); }

    bool isFinished() { return m_isFinished; }
private:
    std::thread m_thread;
    bool m_isFinished;
};

//sock和对应的通信线程
std::map<int, CThread> mapSockThread;

//和sock代表的客户通信
void communication(int sock)
{
    constexpr int maxSize = 2048;
    char buff[maxSize];

    while(true)
    {
        int recvBytes = recv(sock, buff, maxSize, 0);

        if(recvBytes <= 0)
            break;
        
        std::cout << "recv : " << std::string(buff, recvBytes) << " from sock : " << sock << std::endl;
        
        std::string message = "fuck! do not disturb me!!!";
        int sendBytes = send(sock, message.c_str(), message.size(), 0);
    }
}

//监听想连接serverSock的请求
void listenConnection(int serverSock)
{
    sockaddr_in remoteAddr;
    int size = sizeof(remoteAddr);

    while(true)
    {
        std::vector<int> vecSock;
        //找到所有已经通信结束了的
        for(auto i = mapSockThread.begin(); i != mapSockThread.end(); ++i)
        {
            if(i->second.isFinished())
                vecSock.push_back(i->first);
        }
        //清掉那些已经通信结束了的
        for(auto i = vecSock.begin(); i != vecSock.end(); ++i)
        {
            closesocket(*i);
            mapSockThread.erase(*i);
            std::cout << "********erase : " << *i << "**********\n";
        }

        int remoteSock = accept(serverSock, (sockaddr*)(&remoteAddr), &size);
        
        if(remoteSock != INVALID_SOCKET)
        {
            mapSockThread.insert({ remoteSock, CThread() });
            mapSockThread.at(remoteSock).run(communication, remoteSock);
        }
        
    }
}

int main()
{
    std::cout << "hello server ... " << std::endl;

    //window下的socket通信,必须调用这个函数来初始化相应的模块
    WSAData wsa;
    WSAStartup(MAKEWORD(2, 2), &wsa);

    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if(sock == INVALID_SOCKET)
    {
        std::cout << "socket error ...\n";
        return 0;
    }

    sockaddr_in sockAddr;
    std::memset(&sockAddr, 0, sizeof(sockAddr));
    sockAddr.sin_family = AF_INET;
    sockAddr.sin_port = htons(8888);

    if(bind(sock, (const sockaddr*)(&sockAddr), sizeof(sockAddr)) == -1)
    {
        std::cout << "bind error ...\n";
        return 0;
    }

    if(listen(sock, 5) == -1)
    {
        std::cout << "listen error ...\n";
        return 0;
    }

    std::thread listenThread(listenConnection, sock);
    listenThread.join();

    //释放服务器资源
    closesocket(sock);

    //释放资源
    WSACleanup();

    return 0;
}

2.2:client.cpp

#include <iostream>
#include <string>

#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib");

int main()
{
    std::cout << "hello client ...\n";

    WSAData wsa;
    WSAStartup(MAKEWORD(2, 2), &wsa);

    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if(sock == INVALID_SOCKET)
    {
        std::cout << "socket error ...\n";
        return 0;
    }

    //要连接的服务器地址
    sockaddr_in serverAddr;
    std::memset(&serverAddr, 0, sizeof(serverAddr));

    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(8888);
    //这里你可以用其他方法设置ip地址
    //这里是127.0.0.1代表本机,想连接其他电脑,换成对应电脑ip即可
    serverAddr.sin_addr.S_un.S_un_b.s_b1 = 127;
    serverAddr.sin_addr.S_un.S_un_b.s_b2 = 0;
    serverAddr.sin_addr.S_un.S_un_b.s_b3 = 0;
    serverAddr.sin_addr.S_un.S_un_b.s_b4 = 1;

    //请求将本地sock和服务器serverAddr连接起来
    if(connect(sock, (sockaddr*)(&serverAddr), sizeof(serverAddr)) == -1)
    {
        std::cout << "connect error ...\n";
        return 0;
    }

    constexpr int maxSize = 2048;
    char buff[maxSize];
    std::string message;

    while(true)
    {
        std::cout << "send : ";
        std::cin >> message;

        if(message == "quit")
            break;

        int sendBytes = send(sock, message.c_str(), message.size(), 0);

        if(sendBytes > 0)
        {
            int recvBytes = recv(sock, buff, maxSize, 0);
            if(recvBytes > 0)
            {
                std::cout << "recv : " << std::string(buff, recvBytes) << std::endl;
            }
        }
    }

    closesocket(sock);
}

三、demo运行

1:运行server

2:运行client(可以运行多个client,即多个client连接同一个服务器)

3:在client中随便输入一些数据,然后回车,服务器会回“fuck! do not disturb me!!!”,当然你可以找到对应的代码进行修改

 

首先要理解基本的原理,2台电脑间实现TCP通讯,首先要建立起连接,在这里要提到服务器端与客户端,两个的区别通俗讲就是主动与被动的关系,两个人对话,肯定是先有人先发起会话,要不然谁都不讲,谈什么话题,呵呵!一样,TCPIP下建立连接首先要有一个服务器,它是被动的,它只能等待别人跟它建立连接,自己不会去主动连接,那客户端如何去连接它呢,这里提到2个东西,IP地址和端口号,通俗来讲就是你去拜访某人,知道了他的地址是一号大街2号楼,这个是IP地址,那么1号楼这么多门牌号怎么区分,嗯!门牌号就是端口(这里提到一点,我们访问网页的时候也是IP地址和端口号,IE默认的端口号是80),一个服务器可以接受多个客户端的连接,但是一个客户端只能连接一台服务器,在连接后,服务器自动划 分内存区域以分配各个客户端的通讯,那么,那么多的客户端服务器如何区分,你可能会说,根据IP么,不是很完整,很简单的例子,你一台计算机开3个QQ,服务器怎么区分?所以准确的说是IP和端口号,但是客户端的端口号不是由你自己定的,是由计算机自动分配的,要不然就出现端口冲突了,说的这么多,看下面的这张图就简单明了了。 在上面这张图中,你可以理解为程序A和程序B是2个SOCKET程序,服务器端程序A设置端口为81,已接受到3个客户端的连接,计算机C开了2个程序,分别连接到E和D,而他的端口是计算机自动分配的,连接到E的端口为789,连接到D的为790。 了解了TCPIP通讯的基本结构后,接下来讲解建立的流程,首先声明一下我用的开发环境是Visual Studio2008版的,语言C#,组件System.Net.Sockets,流程的建立包括服务器端的建立和客户端的建立,如图所示: 二、实现: 1.客户端: 第一步,要创建一个客户端对象TcpClient(命名空间在System.Net.Sockets),接着,调用对象下的方法BeginConnect进行尝试连接,入口参数有4个,address(目标IP地址),port(目标端口号),requestCallback(连接成功后的返调函数),state(传递参数,是一个对象,随便什么都行,我建议是将TcpClient自己传递过去),调用完毕这个函数,系统将进行尝试连接服务器。 第二步,在第一步讲过一个入口参数requestCallback(连接成功后的返调函数),比如我们定义一个函数void Connected(IAsyncResult result),在连接服务器成功后,系统会调用此函数,在函数里,我们要获取到系统分配的数据流传输对象(NetworkStream),这个对象是用来处理客户端与服务器端数据传输的,此对象由TcpClient获得,在第一步讲过入口参数state,如果我们传递了TcpClient进去,那么,在函数里我们可以根据入口参数state获得,将其进行强制转换TcpClient tcpclt = (TcpClient)result.AsyncState,接着获取数据流传输对象NetworkStream ns = tcpclt.GetStream(),此对象我建议弄成全局变量,以便于其他函数调用,接着我们将挂起数据接收等待,调用ns下的方法BeginRead,入口参数有5个,buff(数据缓冲),offset(缓冲起始序号),size(缓冲长度),callback(接收到数据后的返调函数),state(传递参数,一样,随便什么都可以,建议将buff传递过去),调用完毕函数后,就可以进行数据接收等待了,在这里因为已经创建了NetworkStream对象,所以也可以进行向服务器发送数据的操作了,调用ns下的方法Write就可以向服务器发送数据了,入口参数3个,buff(数据缓冲),offset(缓冲起始序号),size(缓冲长度)。 第三步,在第二步讲过调用了BeginRead函数时的一个入口参数callback(接收到数据后的返调函数),比如我们定义了一个函数void DataRec(IAsyncResult result),在服务器向客户端发送数据后,系统会调用此函数,在函数里我们要获得数据流(byte数组),在上一步讲解BeginRead函数的时候还有一个入口参数state,如果我们传递了buff进去,那么,在这里我们要强制转换成byte[]类型byte[] data= (byte[])result.AsyncState,转换完毕后,我们还要获取缓冲区的大小int length = ns.EndRead(result),ns为上一步创建的NetworkStream全局对象,接着我们就可以对数据进行处理了,如果获取的length为0表示客户端已经断开连接。 具体实现代码,在这里我建立了一个名称为Test的类: 2.服务器端: 相对于客户端的实现,服务器端的实现稍复杂一点,因为前面讲过,一个服务器端可以接受N个客户端的连接,因此,在服务器端,有必要对每个连接上来的客户端进行登记,因此服务器端的程序结构包括了2个程序结构,第一个程序结构主要负责启动服务器、对来访的客户端进行登记和撤销,因此我们需要建立2个类。 第一个程序结构负责服务器的启动与客户端连接的登记,首先建立TcpListener网络侦听类,建立的时候构造函数分别包括localaddr和port2个参数,localaddr指的是本地地址,也就是服务器的IP地址,有人会问为什么它自己不去自动获得本机的地址?关于这个举个很简单的例子,服务器安装了2个网卡,也就有了2个IP地址,那建立服务器的时候就可以选择侦听的使用的是哪个网络端口了,不过一般的电脑只有一个网络端口,你可以懒点直接写个固定的函数直接获取IP地址System.Net.Dns.GetHostAddresses(System.Net.Dns.GetHostName())[0],GetHostAddresses函数就是获取本机的IP地址,默认选择第一个端口于是后面加个[0],第2个参数port是真侦听的端口,这个简单,自己决定,如果出现端口冲突,函数自己会提醒错误的。第二步,启动服务器,TcpListener.Start()。第三步,启动客户端的尝试连接,TcpListener.BeginAcceptTcpClient,入口2个参数,callback(客户端连接上后的返调函数),state(传递参数,跟第二节介绍的一样,随便什么都可以,建立把TcpListener自身传递过去),第四步,建立客户端连接上来后的返调函数,比如我们建立个名为void ClientAccept(IAsyncResult result)的函数,函数里,我们要获取客户端的对象,第三步里讲过我们传递TcpListener参数进去,在这里,我们通过入口参数获取它TcpListener tcplst = (TcpListener)result.AsyncState,获取客户端对象TcpClient bak_tcpclient = tcplst.EndAcceptTcpClient(result),这个bak_tcpclient我建议在类里面建立个列表,然后把它加进去,因为下一个客户端连接上来后此对象就会被冲刷掉了,客户端处理完毕后,接下来我们要启动下一个客户端的连接tcplst.BeginAcceptTcpClient(new AsyncCallback(sub_ClientAccept), tcplst),这个和第三步是一样的,我就不重复了。 第二个程序结构主要负责单个客户端与服务器端的处理程序,主要负责数据的通讯,方法很类似客户端的代码,基本大同,除了不需要启动连接的函数,因此这个程序结构主要启动下数据的侦听的功能、判断断开的功能、数据发送的功能即可,在第一个程序第四步我们获取了客户端的对象bak_tcpclient,在这里,我们首先启动数据侦听功能NetworkStream ns= bak_tcpclient.GetStream();ns.BeginRead(data, 0, 1024, new AsyncCallback(DataRec), data);这个跟我在第二节里介绍的是一模一样的(第二节第10行),还有数据的处理函数,数据发送函数,判断连接已断开的代码与第二节也是一模一样的,不过在这里我们需要额外的添加一段代码,当判断出连接已断开的时候,我们要将客户端告知第一个程序结构进行删除客户端操作,这个方法我的实现方法是在建立第二个程序结构的时候,将第一个程序结构当参数传递进来,判断连接断开后,调用第一个程序结构的公开方法去删除,即从客户端列表下删除此对象。 第一个程序结构我们定义一个TSever的类,第二个程序结构我们一个TClient的类,代码如下:TSever类
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值