网络编程一(服务器、客户端)

 编写一个服务器socket程序步骤:

启动Windows socket环境->创建套接字->绑定套接字->进入监听状态->等待客户端连接->接受客户端请求->向客户端发送数据->关闭套接字->关闭Windows socket环境

 

头文件

//注意将WinSock2放在之前 否则会报错
//或者使用宏定义
#include<WinSock2.h>
#include<Windows.h>
//明确指定需要一个动态库
//在工程中连接器中加入lib动态库
#pragma comment(lib,"ws2_32.lib")

 

 

启动Windows socket环境

   //调用windows库文件
    //启动Windows socket 2.x环境
    WORD ver = MAKEWORD(2, 2);
    WSADATA dat;
    WSAStartup(ver, &dat);

 

 

创建套接字

套接字就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。

创建套接字需要使用到socket()函数。

Linux:int socket(int af,int type,int protocol);

Windows:SOCKET socket(int af,int type,int protocol);

  //1、建立一个socket 套接字
    SOCKET tcp_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

其中的区别是返回类型不同,Linux下返回的是int型的文件描述符,而Windows下返回的是SOCKET类型的套接字。

Linux中一切都是文件,每个文件都有一个整数类型的文件描述符。socket也是一个文件,使用socket创建套接字后就返回一个int类型的文件描述符。

Windows下回区分普通文件和socket文件,在使用socket创建套接字后返回值类型为SOCKET类型用于表示一个套接字。

 

int af:为地址族(Address Family),就是IP的类型型(AF_INET,AF_INET6)分别代表IPv4和IPv6地址。

例:127.0.0.1就是一个IPv4地址,通常用来表示本机地址。

 

int type:表示数据常熟方式/套接字类型。

SOCK_STREAM(流数据格式/面向连接的套接字):一种可靠地、双向的数据通信数据流,数据有误,可以重新发送。一般使用TCP协议。

流格式套接字的内部有一个缓冲区(字符数组),通过socket传输 的数据将保存到这个缓冲区。接收端可以选择性的读取缓冲区的内容。

浏览器所使用的http协议就是基于面向连接的套接字。

 

SOCK_DDGRAM(数据报套接字/无连接的套接字):面向无连接的,传输数据中不做数据校验,如果数据出错,无法重传。也正因如此在传输效率上要比流数据格式套接字。一般使用UDP协议。

QQ视频聊天和微信语音聊天使用到SOCK_DDGRAM传输数据。

 

int protocol:表示传输协议,IPPROTO_TCPIPPTOTO_UDP协议分别表示TCP传输协议和UDP传输协议。

 

绑定套接字

绑定套接字需要使用bind()函数。

Linux:int bind(int sock,struct sockaddr* addr, socklen_t addrlen);

Windows:int bind(SOCKET sock, const struct sockaddr* addr, int addrlen);

  //2、bind 绑定用于接受客户端连接的 网路端口
    sockaddr_in _sin = {};
    _sin.sin_family = AF_INET;
    _sin.sin_port = htons(4567);//绑定一个端口号
    _sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");    //绑定到那个ip地址
    bind(_sock, (sockaddr*)&_sin, sizeof(_sin))  //将套接字和IP、端口绑定

SOCKET sock:表示前面使用socket()函数创建的套接字对象。

const struct sockaddr* addr:一个结构体用来表示IP、端口号等

创建sockaddr_in结构体,通过该结构体设置IP地址127.0.0.1(本机ip)、端口2345。

sockaddr_in结构体如下所示:

  typedef struct sockaddr_in {
  #if(_WIN32_WINNT < 0x0600)
      short   sin_family;   //地址族(Address Family),也就是地址类型
  #else 
      ADDRESS_FAMILY sin_family;
  #endif 
      USHORT sin_port;     //16位的端口号
      IN_ADDR sin_addr;   //32位IP地址
      CHAR sin_zero[8];    //不使用,一般用0填充
  } SOCKADDR_IN, *PSOCKADDR_IN;

其中宏定义表示:当Windows的版本小于0x0600使用地址族类型为 short型,否则使ADDRESS_FAMILY类型。ADDRESS_FAMILY就是 unsigned short类型。typedef unsigned short USHORT;

sin_family:和socket()的第一个参数的含义相同,取值也必须保持一致。

sin_port:端口号,两个字节16位表示,理论上端口号的取值范围为0~65535,0~1023为系统端口号,端一般由系统分配给特定的服务程序,如:Web:80、FTP:21。1024~49151为登记端口号,用户最好使用49152~65535之间的端口号。

sin_addr:是一个结构体如下所示。只需要知道使用来绑定IP就行。暂时还搞不太清???

  typedef struct in_addr {
          union {
                  struct { UCHAR s_b1,s_b2,s_b3,s_b4; } S_un_b;
                  struct { USHORT s_w1,s_w2; } S_un_w;
                  ULONG S_addr;
          } S_un;
  #define s_addr  S_un.S_addr /* can be used for most tcp & ip code */
  #define s_host  S_un.S_un_b.s_b2    // host on imp
  #define s_net   S_un.S_un_b.s_b1    // network
  #define s_imp   S_un.S_un_w.s_w2    // imp
  #define s_impno S_un.S_un_b.s_b4    // imp #
  #define s_lh    S_un.S_un_b.s_b3    // logical host
  } IN_ADDR, *PIN_ADDR, FAR *LPIN_ADDR;

int addrlen:为addr变量的大小,可以使用sizeof计算出来。

 

问题:为什么使用sockaddr_in而不使用bind()函数要求的sockaddr?

参考:http://c.biancheng.net/view/2344.html

sockaddr 和 sockaddr_in 的长度相同,都是16字节,只是将IP地址和端口号合并到一起,用一个成员 sa_data 表示。要想给 sa_data 赋值,必须同时指明IP地址和端口号,例如”127.0.0.1:80“,遗憾的是,没有相关函数将这个字符串转换成需要的形式,也就很难给 sockaddr 类型的变量赋值,所以使用 sockaddr_in 来代替。这两个结构体的长度相同,强制转换类型时不会丢失字节,也没有多余的字节。
可以认为,sockaddr 是一种通用的结构体,可以用来保存多种类型的IP地址和端口号,而 sockaddr_in 是专门用来保存 IPv4 地址的结构体。另外还有 sockaddr_in6,用来保存 IPv6 地址

 

进入监听状态

被动进入监听状态需要使用liste()函数。

Linux:int listen(int sock, int backlog);

Windows:int listen(SOCKET, int backlog);

SOCKET sock:表示前面使用socket()函数创建的套接字对象。

int backlog:表示请求队列的最大长度。

  //3、listen 监听网络接口
  SOCKET_ERROR == listen(_sock, 5)

被动监听:只有当客户端请求时,套接字才进入“唤醒”状态来响应请求。否则处于“睡眠”状态

请求队列:

套接字处理客户端请求时,如果有新的请求,套接字将请求放入缓冲区中,处理完当前请求后,再从缓冲区中取出请求。如果不断有新的请求进来,就按先后顺序在缓冲区中排队,直到缓冲区满为止。这个缓冲区,可以称为请求队列。

listen()中第二个参数设置缓冲区的长度,设置为多少根据情况而定。设置为SOMAXCONN则由系统来解决。

当请求队列满了的时候,有新的请求就会发出请求错误。

 

等待客户端连接

使用accept()函数等待客户端请求。

Linux:int accept(int sock, struct sockaddr* addr, socklen_t* addrlen);

Windows:SOCKET accept(SOCKET sock, struct sockaddr* addr, int* addrlen);

    //4、accept 等待接受客户端连接
    sockaddr_in clientAddr = {};
    int nAddrLen = sizeof(sockaddr_in);
    SOCKET _cSock = INVALID_SOCKET;
    _cSock = accept(_sock, (sockaddr*)&clientAddr, &nAddrLen);

struct sockaddr* addr:保存的客户端的IP和端口号,注意!!!

int* addrlen:表示addr的长度,使用sizeof()计算。

使用accept()函数会返回一个新的套接字和客户端进行通信,之后和客户端进行通信的时候使用这个新的套接字而不是之前的套接字。

accept()会阻塞程序的执行,直到有新的请求到来。

 

接受客户端请求

接受客户端请求使用read() / recv()函数。

Linux:ssize_t read(int fd,void* buf, size_t nbytes);

Windows:int recv(SOCKET sock,char* buf, int len, int flags);

  DataHeader header = {};
   //5、接受客户端的请求数据
  int nLen = recv(_cSock, (char*)&header, sizeof(DataHeader), 0);

Linux来说:

int fd:要读取的文件描述符。

void* buf:接受数据的缓冲区地址。

size_t nbytes:要读取的数据的字节数。

read()函数会从fd文件中读取nbytes个字节并保存到缓冲区buf中,成功返回读取到的字节数,失败返回-1.

 

Windows来说:

SOCKET sock:要接受的数据的套接字,不论是客户还是服务器应用程序都用recv函数从TCP连接的另一端接受数据。

char* buf:指定一个缓冲区,用来存放recv函数接受到的数据。

int len:指明buf的长度。

int flags:可选项,可以为0或NULL,一般设置为0。先不用深究。

 

1、recv先等待sock的发送缓冲中的数据被传输协议传送完毕,如果传送失败返回SOCKET_ERROR

2、如果sock的发送缓冲中没有数据或者数据被协议成功发送完毕,recv先检查套接字sock的接受缓冲区。 如果sock接受缓冲区中没有数据或者协议正在接受数据,那么recv就一直等待,知道协议把数据接受完毕。当协议把数据接受完毕,recv函数就把sock的接受缓冲中的数据copy到buf中(即recv中第二个参数)(注意:recv函数只是copy数据,接受数据是由协议完成)。

recv函数返回的是实际copy的字节数。recv在copy时出错,返回SOCKET_ERROR;如果recv函数在等待协议接受数据时网络中断了,返回0。Linux下调用recv的进程会接受到一个SIGPIPE信号,进程对该信号的默认处理是进程终止。

 

 

向客户端发送数据

客户端发送数据使用write() / send()函数。

Linux:ssize_t write(int fd, cons void* buf, size_t nbytes);

Windows:int send(SOCKET sock, const char* buf, int len, int flags);

  struct LoginResult
  {
      int result;
  };

  LoginResult ret = { 1 };
  //6、向客户端发送数据
  send(_cSock, (char*)&ret, sizeof(LoginResult), 0);

 

 

 

关闭套接字

关闭套接字使用closesocke()函数

  //7、closesocket关闭套接字
    closesocket(_sock);

 

 

终止DLL使用

    WSACleanup();

 

 

 编写一个客户端socket程序步骤:

初始化DLL->创建套接字->连接服务器->向服务器发送请求->接受服务器返回数据->关闭套接字->终止DLL使用

创建套接字

与服务器相同,请看服务器处。

 

连接服务器

连接服务器使用connect()函数。

Linux:int connect(int sock, struct sockaddr* serv_addr, socklen_t addrlen);

Windows:int connect(SOCKET sock, const struct sockaddr* serv_addr, int addrlen);

    //2、连接服务器
    sockaddr_in _sin = {};
    _sin.sin_family = AF_INET;
    _sin.sin_port = htons(4567);
    _sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
    int ret = connect(_sock, (sockaddr*)&_sin, sizeof(sockaddr_in));

 

SOCKET sock:客户端使用socket()函数创建的socket。

const struct sockaddr* serv_addr:与客户端bind()一样,请查看服务器对应内容。

int addrlen:为serv_addr变量的大小,可以使用sizeof计算出来。

 

向服务器发送请求

 与服务区发送数据相同。write() / send()

接受服务器返回数据

 与服务器接受请求相同。read() / recv()

关闭套接字

关闭套接字使用closesocke()函数。

 

 

完成代码:

服务器:

#define WIN32_LEAN_AND_MEAN
#include<iostream>

//注意将WinSock2放在之前 否则会报错
//或者使用宏定义
#include<WinSock2.h>
#include<Windows.h>
//明确指定需要一个动态库
//在工程中连接器中加入lib动态库
#pragma comment(lib,"ws2_32.lib")

enum CMD
{
    CMD_LOGIN,
    CMD_LOGIN_RESULT,
    CMD_LOGOUT,
    CMD_LOGOUT_RESULT,
    CMD_ERROR
};
struct DataHeader
{
    short dataLength;
    short cmd;
};//包体继承包头信息
//生成一个完整的包
struct Login :public DataHeader
{
    Login()
    {
        dataLength = sizeof(Login);
        cmd = CMD_LOGIN;
    }
    char userName[32];
    char PassWord[32];
};
struct LoginResult :public DataHeader
{
    LoginResult()
    {
        dataLength = sizeof(LoginResult);
        cmd = CMD_LOGIN_RESULT;
        result = 0;
    }
    int result;
};
//退出
struct Logout :public DataHeader
{
    Logout()
    {
        dataLength = sizeof(Logout);
        cmd = CMD_LOGOUT;
    }
    char userName[32];
};
struct LogoutResult :public DataHeader
{
    LogoutResult()
    {
        dataLength = sizeof(LogoutResult);
        cmd = CMD_LOGOUT_RESULT;
        result = 0;
    }
    int result;
};


int main()
{
    //调用windows库文件
    //启动Windows socket 2.x环境
    WORD ver = MAKEWORD(2, 2);
    WSADATA dat;
    WSAStartup(ver, &dat);
    
    //1、建立一个socket 套接字
    SOCKET _sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

    //将套接字和IP、端口绑定
    //2、bind 绑定IP、端口
    sockaddr_in _sin = {};
    _sin.sin_family = AF_INET;
    _sin.sin_port = htons(4567);//绑定一个端口号
    _sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");    //绑定到那个ip地址
    if (SOCKET_ERROR == bind(_sock, (sockaddr*)&_sin, sizeof(_sin)))
    {
        std::cout << "绑定IP、端口失败~" << std::endl;
    }
    else
    {
        std::cout << "绑定IP、端口成功~" << std::endl;
    }

    //3、listen 监听网络接口
    //#define INVALID_SOCKET  (SOCKET)(~0)
    //#define SOCKET_ERROR            (-1)
    if (SOCKET_ERROR == listen(_sock, 5))
    {
        std::cout << "监听接口开启失败~" << std::endl;
    }
    else
    {
        std::cout << "监听接口开启成功~" << std::endl;
    }

    //4、accept 等待接受客户端连接
    sockaddr_in clientAddr = {};
    int nAddrLen = sizeof(sockaddr_in);

    //为了后续判断时候接受成功??????????
    SOCKET _cSock = INVALID_SOCKET;
    //_cSock为与客户端连接成功的   套接字
    _cSock = accept(_sock, (sockaddr*)&clientAddr, &nAddrLen);
    if (INVALID_SOCKET == _cSock) {
        std::cout << "客户端连接失败~" << std::endl;
    }
    std::cout << "新加入客户端IP:" << inet_ntoa(clientAddr.sin_addr) << std::endl;
    
    while (true)
    {
        DataHeader header = {};
        //5、接受客户端的请求数据
        int nLen = recv(_cSock, (char*)&header, sizeof(DataHeader), 0);
        if (nLen <= 0)
        {
            std::cout << "客户端已退出,任务结束。" << std::endl;
            break;
        }
    
        //通过接收到的header来判断
        //客户端发送的命令是什么
        //同时给客户端反馈
        switch (header.cmd)
        {
        case CMD_LOGIN:
        {
            Login login = {};
            recv(_cSock, (char*)&login+sizeof(DataHeader), sizeof(Login)-sizeof(DataHeader), 0);
            std::cout << "收到命令:" << login.cmd << ";" << std::endl
                << "数据长度:" << login.dataLength << ";" << std::endl
                <<"userName:" << login.userName << ";" << std::endl
                <<"userPass:"<<login.PassWord<<std::endl;
/*
//加入一些操作(判断)
*/

//反馈
LoginResult ret; send(_cSock, (char*)&ret, sizeof(LoginResult), 0); } break; case CMD_LOGOUT: { Logout logout = {}; recv(_cSock, (char*)&logout+ sizeof(DataHeader), sizeof(logout)- sizeof(DataHeader), 0); std::cout << "收到命令:" << logout.cmd<<";" << std::endl << "数据长度:" << logout.dataLength << ";" << std::endl << "userName:" << logout.userName<< std::endl;
/*
//可以加入一些判断操作等...
*/

//反馈
LogoutResult ret; send(_cSock, (char*)&ret, sizeof(ret), 0); } break; default: header.cmd = CMD_ERROR; header.dataLength = 0; send(_cSock, (char*)&header, sizeof(header), 0); break; } } //8、closesocket关闭套接字 closesocket(_sock); WSACleanup(); std::cout << "已退出,任务结束。" << std::endl; system("pause"); return 0; }

 

客户端:

#define _CRT_SECURE_NO_WARNINGS
#define WIN32_LEAN_AND_MEAN
#include<iostream>

//注意将WinSock2放在之前 否则会报错
//或者使用宏定义
#include<WinSock2.h>
#include<Windows.h>
//明确指定需要一个动态库
//在工程中连接器中加入lib动态库
#pragma comment(lib,"ws2_32.lib")

enum CMD
{
    CMD_LOGIN,
    CMD_LOGIN_RESULT,
    CMD_LOGOUT,
    CMD_LOGOUT_RESULT,
    CMD_ERROR
};

//网络数据报文的格式定义
//报文有两个部分 包头和包体
//包头 描述本次消息包的大小 描述数据的作用
//包体 传输数据
struct DataHeader
{
    short dataLength;
    short cmd;
};
//登录
//包体继承包头信息
//生成一个完整的包
struct Login :public DataHeader
{
    Login()
    {
        dataLength = sizeof(Login);
        cmd = CMD_LOGIN;
    }
    char userName[32];
    char PassWord[32];
};
struct LoginResult :public DataHeader
{
    LoginResult()
    {
        dataLength = sizeof(LoginResult);
        cmd = CMD_LOGIN_RESULT;
        result = 0;
    }
    int result;
};
//退出
struct Logout :public DataHeader
{
    Logout()
    {
        dataLength = sizeof(Logout);
        cmd = CMD_LOGOUT;
    }
    char userName[32];
};
struct LogoutResult :public DataHeader
{
    LogoutResult()
    {
        dataLength = sizeof(LogoutResult);
        cmd = CMD_LOGOUT_RESULT;
        result = 0;
    }
    int result;
};

int main()
{
    //调用windows库文件
    //启动Windows socket 2.x环境
    WORD ver = MAKEWORD(2, 2);
    WSADATA dat;
    WSAStartup(ver, &dat);

    //1、建立一个socket
    SOCKET _sock = socket(AF_INET, SOCK_STREAM, 0);
    if (INVALID_SOCKET == _sock)
    {
        std::cout << "建立socket失败~" << std::endl;
    }
    else
    {
        std::cout << "建立socket成功~" << std::endl;
    }
    //2、连接服务器
    sockaddr_in _sin = {};
    _sin.sin_family = AF_INET;
    _sin.sin_port = htons(4567);
    _sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
    int ret = connect(_sock, (sockaddr*)&_sin, sizeof(sockaddr_in));
    if (SOCKET_ERROR == ret)
    {
        std::cout << "连接服务器失败" << std::endl;
    }
    else
    {
        std::cout << "连接服务器成功" << std::endl;
    }
    
    while (true)
    {
        //3、输入请求命令
        char cmdBuf[128] = {};
        std::cin >> cmdBuf;
        //4、处理请求
        if (0 == strcmp(cmdBuf, "exit"))
        {
            std::cout << "收到exit命令,任务结束。" << std::endl;
            break;
        }
        else if (0 == strcmp(cmdBuf, "login"))
        {
            //5、向服务器发送命令
            Login login;
            strcpy(login.userName, "xmq");
            strcpy(login.PassWord, "mima");
            send(_sock, (const char*)&login, sizeof(login), 0);
            //接受服务器返回的数据
            LoginResult loginRet = {};
            recv(_sock, (char*)&loginRet, sizeof(loginRet), 0);
            std::cout << "LoginResult: " << loginRet.result<<std::endl;
        }
        else if (0 == strcmp(cmdBuf, "logout"))
        {
            //5、向服务器发送命令
            Logout logout;
            strcpy(logout.userName, "xmq");
            send(_sock, (const char*)&logout, sizeof(logout), 0);
            //接受服务器返回的数据
            LogoutResult logoutRet = {};
            recv(_sock, (char*)&logoutRet, sizeof(logoutRet), 0);
            std::cout << "LogoutResult: " << logoutRet.result << std::endl;
        }
        else
        {
            std::cout << "不支持命令,请重新输入。" << std::endl;
        }
    }

    //7、closesocket 关闭套接字
    closesocket(_sock);

    WSACleanup();

    std::cout << "已退出,任务结束。" << std::endl;

    system("pause");
    return 0;
}

 

posted @ 2020-08-14 14:41  夏~  阅读(302)  评论(0编辑  收藏  举报