套接字网络通信

 视频解析:【C/C++网络编程】多客户端聊天室!多线程+网络编程实现多人聊天功能,可以实现多客户端的

#pragma once
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#pragma comment(lib, "Ws2_32.lib")
#include"winsock2.h"
#include"ws2tcpip.h"
#include<iostream>
#include<string>
#include<cstring>
const static uint16_t defalu_port = 888;
const static std::string defaul_ip = "127.0.0.1";
const static int default_buf_size = 1024;

void Usage(std::string proc)
{
    std::cout << "Usage\t" << "local-ip, local_port\n" << std::endl;

}

int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        return 1;
    }

    std::string _ip = argv[1];
    uint16_t _port = std::stoi(argv[2]);

    //第一步确定协议版本
    WSAData wsadata;
    WSAStartup(MAKEWORD(2, 2), &wsadata);
    if (LOBYTE(wsadata.wVersion) != 2 ||
        HIBYTE(wsadata.wVersion) != 2)
    {
        printf("确定网络协议版本失败!\n");
        system("pause");
        return 1;
    }
    else printf("确定网络协议版本成功!\n");

    int sockedfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockedfd < 0)
    {
        std::cout << "create socket failed" << std::endl;
        return 1;
    }


    //1.创建socket
   int _sockfd = socket(AF_INET, SOCK_DGRAM, 0);  //报式套接字,第三个参数默认UDP协议
    if (_sockfd == -1)
    {
        printf("create socket failed,socket error:%d\n", errno);
        exit(SOCKET_ERROR);
    }
    printf("create socket success,sockfd:%d\n", _sockfd);

    //2.确定协议ip
    struct sockaddr_in server;
    server = { 0 };
    server.sin_family = AF_INET;
    server.sin_port = htons(_port);
    server.sin_addr.S_un.S_addr = inet_addr(_ip.c_str());


    while (true)
    {
        //向服务器发送数据
        std::string send_buf;
        std::cout << "请输入要发送的数据:" << std::endl;
        std::getline(std::cin, send_buf);
        size_t n = sendto(_sockfd, send_buf.c_str(), send_buf.size(), 0, (struct sockaddr*)&server, sizeof(server));

        //发送完毕,接收服务器返回的数据
        if (n > 0)
        {
            struct sockaddr_in remote;
            socklen_t addr_len = sizeof(remote);
            char recv_buf[default_buf_size];


            int len = recvfrom(_sockfd, recv_buf, default_buf_size, 0, (struct sockaddr*)&remote, &addr_len);
            if (len < 0)
            {
                printf("recvfrom error:%d\n", errno);
                break;
            }
            else
            {
                recv_buf[len] = 0;
                printf("recvfrom success,data:%s\n", recv_buf);
            }
        }
        closesocket(_sockfd);

    }
    



    return 0;
}

【C/C++网络编程】多客户端聊天室!多线程+网络编程实现多人聊天功能,可以实现多客户端的

网络视频:socket到底是什么?_哔哩哔哩_bilibili

大丙解析:13-客户端连接服务器函数 connect_哔哩哔哩_bilibili

目录

视频解析:【C/C++网络编程】多客户端聊天室!多线程+网络编程实现多人聊天功能,可以实现多客户端的简单通讯 ~_哔哩哔哩_bilibili

网络视频:socket到底是什么?_哔哩哔哩_bilibili

大丙解析:13-客户端连接服务器函数 connect_哔哩哔哩_bilibili

客户端

服务器端进行网络通信的流程

 1.初始化套接字环境

 2.创建Socket

3.确定服务器协议地址簇 

 4.绑定

 5.监听​编辑

 6.接受客户端连接​编辑

7.通信

 8.关闭socket

9.清理协议

 客户端进行网络通信的流程

    4.连接服务器​编辑

5.通信

6.关闭socket

7.清理协议

代码整理 

客户端

服务器端


前置知识

套接字-Socket | 爱编程的大丙

客户端

服务器端进行网络通信的流程

1.初始化套接字环境

 2.创建Socket

3.确定服务器协议地址簇

4.绑定

5.监听

6.接收客户端链接

7.通信

8.关闭socket

 1.初始化套接字环境

先声明一个WSAData类型的变量

WSAData wsadata;

然后输入:"WSAStartup",选中它,按F1会转到帮助文档. 文档里说明了要使用WSAStartup的要求:

 通过MAKEWORD这个宏指定主版本为2,副版本为2的协议:

然后加上判断

WSAStartup(MAKEWORD(2, 2), &wsadata);
if (LOBYTE(wsadata.wVersion) != 2 ||
		HIBYTE(wsadata.wVersion) != 2)
	{
		printf("确定网络协议版本失败!\n");
		system("pause");
		return -1;
	}
	else printf("确定网络协议版本成功!\n");

 2.创建Socket

接口:socket():

 第一个参数指定要使用的ip协议是Ipv4还是ipv6.

这由两个宏定义:

其中AF_UNIX和AF_LOCAL不同于基于网络的套接字如 AF_INET(用于IPv4)或 AF_INET6(用于IPv6),它并不通过网络进行数据交换,而是利用了操作系统的内核机制,在进程之间直接传递数据。。

  • 参数
    • 第二个参数int type指定了要使用的协议是流式协议还是报式协议。 
    • 流式协议的宏定义为SOCK_STREAM,报式协议的宏定位为SOCK_DGRAM。
    • 第三个参数指定使用什么协议,流式协议默认使用Tcp,报式协议默认使用udp。
  • 返回值
    • 如果成功就会创建一个文件描述符返回,如果失败就会返回-1
	SOCKET socketserver = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
	if (SOCKET_ERROR == socketserver)
	{
		printf("创建socket失败,%d\n",GetLastError());
		WSACleanup();  //如果失败就把协议清理掉
		system("pause");
		return -1;
	}
	else printf("创建socket成功!\n");

3.确定服务器协议地址簇 

 SOCKADDR_IN就是一个结构体,里面有IP地址,端口号这些成员变量.一个IP地址确定一台主机,端口号确定APP;

    SOCKADDR_IN addr = { 0 };                           
	addr.sin_family = AF_INET;                           //照抄上面的
	addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");  //ip地址
	addr.sin_port = htons(9527);                         //端口号

 4.绑定

绑定接口函数:int bind():

  • 参数
    •  第一个参数是套接字描述符。
    • 第二个参数是一个结构体类型的参数,用来存放要绑定的ip地址和端口号。结构体定义如下:
    • 第三个参数是前面结构体的大小。我们用sizeof()计算出大小后填写到第三个参数即可。
  • 返回值
    • 绑定成功返回0,失败返回-1

	int r = bind(socketserver,(sockaddr*)&addr,sizeof addr);
	if (-1== r)
	{
		printf("绑定失败,%d\n", GetLastError());
		WSACleanup();  //如果失败就把协议清理掉
		system("pause");
		return -1;
	}
	else printf("绑定成功!\n");

 5.监听

  • 参数
    •  第一个参数就是文件描述符,通过调用socket()得到。
    • 第二个参数是指定要绑定的客户端的最大连接个数。最大个数为128个。

注意,这里的的最大连接个数128不是指总共只能连128个客户端机子,而是说一次只能最多连128个客户端机子。n个客户端机子可以分多次进行连接。

  • 返回值
    • 监听成功返回0,失败返回-1
	r = listen(socketserver,10);
	if (-1 == r)
	{
		printf("监听失败,%d\n", GetLastError());
		WSACleanup();  //如果失败就把协议清理掉
		system("pause");
		return -1;
	}
	else printf("监听成功!\n");

 6.接受客户端连接

  •  参数
    • 第一个参数是文件描述符,通过调用socket()得到。
    • 第二个参数仍然是一个结构体类型的指针,用来存储要绑定的客户端的ip地址和端口号。
    • 第三个参数指定第二个参数的大小,用sizeof()计算一下大小即可。
  • 返回值
    • 函数调用成功,得到一个文件描述符, 用于和建立连接的这个客户端通信,调用失败返回 -1

		SOCKET clientSocket = accept(socketserver, (sockaddr*)NULL, NULL);  //后两个参数是客户端的ip,端口号
		if (-1 == clientSocket)
		{
			printf("服务器崩溃,%d\n", GetLastError());
			WSACleanup();  //如果失败就把协议清理掉
			system("pause");
			return -1;
		}
		else printf("有客户端链接服务器\n");
		CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)tongxun, (LPVOID)i, NULL, NULL);
	

7.通信

  •  通信函数
    • 接收  recv/read
    • 发送
	char buff[56];  //接收客户端发来的信息
	while (1)
	{
		r = recv(clientSocket, buff, 55, NULL); //最多接收55条,因为还要留一个给结束符
		if (r > 0)                         //如果接收到消息了就添加一个结束符
		{
			buff[r] = 0;
			printf(">>%s\n", buff);
		}
	}

 8.关闭socket

   closesocket(socketserver);

9.清理协议

   WSACleanup();	

 客户端进行网络通信的流程

1.确定协议版本

 2.创建Socket

3.确定服务器协议地址簇

4.连接服务器

5.通信

6.关闭socket

7.清理协议

1,2,3步骤和服务器端一样,第四个步骤如下:

    4.连接服务器

  •  参数
    • 同accept

          我们注意到第二个参数加了const限定,说明这是一个输入性参数。

  • 返回值

            我们注意看返回值说的是如果绑定或者连接成功就会返回0,失败则返回-1。客户端也要绑定吗?

答案是是的,客户端也需要绑定。那么这个要绑定的ip和端口号是什么呢?

实际上绑定的是一个随机的没有被占用的端口号,这个是不需要我们去手动操作的。

先前我们服务器端绑定的时候是我们手动绑定一个固定ip和端口号,为什么到客户端这里就只需要随机绑定一个端口号了呢?

因为服务器端是优于客户端先创建的,那么客户端如果想向服务器端发送请求就需要知道服务器端的ip地址和端口号,因此服务器端需要一个固定的ip地址和端口号。

服务器端绑定固定ip和客户端成功之后,客户端通过connect()自动与这个固定的ip和端口号连接。

之后服务器端通过accept()函数接受客户端连接,同时接受到了客户端的ip和端口号,然后就可以与客户端进行通信了。

如果我们想让服务器端与客户端的固定的端口号进行通信,那我们在客户端也需要调用bind()函数绑定指定端口号。

    int r = connect(socketserver, (sockaddr*)&addr, sizeof addr);
	if (-1 == r)
	{
		printf("连接服务器失败!\n");
		WSACleanup();  //如果失败就把协议清理掉
		system("pause");
	}
	else printf("连接服务器成功!\n");

5.通信

  • 通信函数
    •  接收  recv/read
    • 发送
	//循环接收用户输入,发送给服务器
	char buff[56];
	while (1)
	{
			scanf("%s", buff);
			send(socketserver, buff, strlen(buff), NULL);
	
	}

6.关闭socket

closesocket(socketserver);

7.清理协议

WSACleanup();


此时,只能做到客户端与服务器端一对一通信:

我们现在想实现多个客户端与服务器端进行通信,就要用到io多路复用,多线程等技术.我们这里用多线程.

我们在接收客户端连接这一部分进行修改:

我们将上文定义的SOCKET clientSocket变为SOCKET clientSocket[].这样就可以接受多条客户端的链接.我们设置一个for循环,在循环中通过下标自增来接受多个客户端链接的请求,代码如下:

for (int i = 0; i < NUM; i++)  //可以接受NUM个客户端的链接
 	{
		clientSocket[i] = accept(socketserver, (sockaddr*)NULL, NULL);  //后两个参数是客户端的ip,端口号
		if (-1 == clientSocket[i])
		{
			printf("服务器崩溃,%d\n", GetLastError());
			WSACleanup();  //如果失败就把协议清理掉
			system("pause");
			return -1;
		}
		else printf("有客户端链接服务器\n");
		CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)tongxun, (LPVOID)i, NULL, NULL);
	}

这样我们就可以多个客户端与服务器端进行通信了:

代码整理 

客户端

#define _CRT_SECURE_NO_WARNINGS 1
#define _WINSOCK_DEPRECATED_NO_WARNINGS  //为了消除警告

#include<stdio.h>

#include <winsock2.h>
#include<windows.h>
#pragma comment(lib, "ws2_32.lib")

int main()
{

	//第一步确定协议版本
	WSAData wsadata;
	WSAStartup(MAKEWORD(2, 2), &wsadata);
	if (LOBYTE(wsadata.wVersion) != 2 ||
		HIBYTE(wsadata.wVersion) != 2)
	{
		printf("确定网络协议版本失败!\n");
		system("pause");
		return -1;
	}
	else printf("确定网络协议版本成功!\n");


	//第二步创建Socket
	SOCKET socketserver = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (SOCKET_ERROR == socketserver)
	{
		printf("创建socket失败,%d\n", GetLastError());
		WSACleanup();  //如果失败就把协议清理掉
		system("pause");
		return -1;
	}
	else printf("创建socket成功!\n");

	//3.确定服务器协议地址簇
	SOCKADDR_IN addr = { 0 };                            //SOCKADDR_IN就是一个结构体
	addr.sin_family = AF_INET;                           //照抄上面的
	addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");  //ip地址
	addr.sin_port = htons(9527);                         //端口号

	//4.连接服务器
	int r = connect(socketserver, (sockaddr*)&addr, sizeof addr);
	if (-1 == r)
	{
		printf("连接服务器失败!\n");
		WSACleanup();  //如果失败就把协议清理掉
		system("pause");
	}
	else printf("连接服务器成功!\n");




	//5.通信
	//循环接收用户输入,发送给服务器
	char buff[56];
	while (1)
	{
			scanf("%s", buff);
			send(socketserver, buff, strlen(buff), NULL);
	
	}


	//6.关闭socket
	closesocket(socketserver);

	//7.清理协议
	WSACleanup();

	return 0;
}

服务器端

#define _CRT_SECURE_NO_WARNINGS 1
#define _WINSOCK_DEPRECATED_NO_WARNINGS  //为了消除警告

#include<stdio.h>

#include <winsock2.h>
#include<windows.h>
#pragma comment(lib, "ws2_32.lib")

#define NUM 1024
SOCKET clientSocket[1024];


void tongxun(int idx);
int main()
{

	//第一步确定协议版本
	WSAData wsadata;
	WSAStartup(MAKEWORD(2, 2), &wsadata);
	if (LOBYTE(wsadata.wVersion) != 2 ||
		HIBYTE(wsadata.wVersion) != 2)
	{
		printf("确定网络协议版本失败!\n");
		system("pause");
		return -1;
	}
	else printf("确定网络协议版本成功!\n");
    

	//第二步创建Socket
	SOCKET socketserver = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
	if (SOCKET_ERROR == socketserver)
	{
		printf("创建socket失败,%d\n",GetLastError());
		WSACleanup();  //如果失败就把协议清理掉
		system("pause");
		return -1;
	}
	else printf("创建socket成功!\n");

	//3.确定服务器协议地址簇
	SOCKADDR_IN addr = { 0 };                            //SOCKADDR_IN就是一个结构体
	addr.sin_family = AF_INET;                           //照抄上面的
	addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");  //ip地址
	addr.sin_port = htons(9527);                         //端口号

	//4.绑定
	int r = bind(socketserver,(sockaddr*)&addr,sizeof addr);
	if (-1== r)
	{
		printf("绑定失败,%d\n", GetLastError());
		WSACleanup();  //如果失败就把协议清理掉
		system("pause");
		return -1;
	}
	else printf("绑定成功!\n");

	//5.监听
	r = listen(socketserver,10);
	if (-1 == r)
	{
		printf("监听失败,%d\n", GetLastError());
		WSACleanup();  //如果失败就把协议清理掉
		system("pause");
		return -1;
	}
	else printf("监听成功!\n");
	

	//6.接收客户端链接
	for (int i = 0; i < NUM; i++)  //可以接受NUM个客户端的链接
 	{
		clientSocket[i] = accept(socketserver, (sockaddr*)NULL, NULL);  //后两个参数是客户端的ip,端口号
		if (-1 == clientSocket[i])
		{
			printf("服务器崩溃,%d\n", GetLastError());
			WSACleanup();  //如果失败就把协议清理掉
			system("pause");
			return -1;
		}
		else printf("有客户端链接服务器\n");
		CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)tongxun, (LPVOID)i, NULL, NULL);
	}
	

	//8.关闭socket
	closesocket(socketserver);
	
	//9.清理协议
	WSACleanup();	



	return 0;
}
void tongxun(int idx)
{
	int r;
	//7.通信
	char buff[56];  //接收客户端发来的信息
	while (1)
	{
		r = recv(clientSocket[idx], buff, 55, NULL); //最多接收55条,因为还要留一个给结束符
		if (r > 0)                         //如果接收到消息了就添加一个结束符
		{
			buff[r] = 0;
			printf(">>%s\n", buff);
		}
	}
}

udp通信

服务器端

首先创建一个udpserver.hpp文件。该文件就是udp通信的服务器端

udpserver.hpp

#pragma once
#include<iostream>
#include"nocopy.hpp"

// static const uint16_t=888;

class UdpServer : public nocopy
{
public:
    UdpServer(std::string& ip, uint16_t prot) :_ip(ip), _port(prot) {}
    ~UdpServer() ;
    void Init() ;
    void Start() ;
private:
    std::string _ip;
    uint16_t _port;
};

我们在该文件中声明一个UdpServer类,类内定义两个成员变量:ip和prot。构造函数和析构,以及Init()和Start()函数。

建立Main.cc文件

Main.cc

首先我们声明一个智能指针对象,用它来管理内存释放。

#include<iostream>
#include<memory>
#include"udpserver.hpp"
#include"Common.hpp"
#include<string>


int main(int argc, char* argv[])
{


    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(ip, port);
    usvr->Init();
    usvr->Start();

    return 0;
}

我们想在程序运行时就输入ip和prot参数。

如果用户不会使用,没有传入ip和prot。我们就引入一个报错机制,提示用户应该传入ip和prot。

Main.cc

void Usage(std::string proc)
{
    std::cout << "Usage\t" << "local-ip, local_port\n" << std::endl;

}
int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        return Usage_Err;
    }

    std::string ip = argv[1];
    uint16_t port = std::stoi(argv[2]);

    return 0;
}

 Common.hpp

#pragma once

enum {
	Usage_Err = 1
};

为了保证服务器端只存在一份,不被拷贝。我们新建一个nocopy类,在该类内部把拷贝构造和赋值重载禁了。然后让UdpServer类继承nocopy类,这样UdpServer类同样也不能被拷贝和赋值。

#pragma once

#include<iostream>

class nocopy
{
public:
    nocopy() {}
    nocopy(const nocopy&) = delete;
    const nocopy& operator=(const nocopy&) = delete;
    ~nocopy() {}
};

此时运行程序,如果没有输入ip和Port就会打印错误码,然后提示传入ip和port:

否则,就会正常执行:

 

通信的几个步骤

udpserver.hpp

服务器端启动:

客户端 

客户端和服务端类似,但是客户端不需要显示绑定,因为客户端的ip是随机绑定的。

#pragma once
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#pragma comment(lib, "Ws2_32.lib")
#include"winsock2.h"
#include"ws2tcpip.h"
#include<iostream>
#include<string>
#include<cstring>
const static uint16_t defalu_port = 888;
const static std::string defaul_ip = "127.0.0.1";
const static int default_buf_size = 1024;

void Usage(std::string proc)
{
    std::cout << "Usage\t" << "local-ip, local_port\n" << std::endl;

}

int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        return 1;
    }

    std::string _ip = argv[1];
    uint16_t _port = std::stoi(argv[2]);

    //第一步确定协议版本
    WSAData wsadata;
    WSAStartup(MAKEWORD(2, 2), &wsadata);
    if (LOBYTE(wsadata.wVersion) != 2 ||
        HIBYTE(wsadata.wVersion) != 2)
    {
        printf("确定网络协议版本失败!\n");
        system("pause");
        return 1;
    }
    else printf("确定网络协议版本成功!\n");

    int sockedfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockedfd < 0)
    {
        std::cout << "create socket failed" << std::endl;
        return 1;
    }


    //1.创建socket
   int _sockfd = socket(AF_INET, SOCK_DGRAM, 0);  //报式套接字,第三个参数默认UDP协议
    if (_sockfd == -1)
    {
        printf("create socket failed,socket error:%d\n", errno);
        exit(SOCKET_ERROR);
    }
    printf("create socket success,sockfd:%d\n", _sockfd);

    //2.确定协议ip
    struct sockaddr_in server;
    server = { 0 };
    server.sin_family = AF_INET;
    server.sin_port = htons(_port);
    server.sin_addr.S_un.S_addr = inet_addr(_ip.c_str());


    while (true)
    {
        //向服务器发送数据
        std::string send_buf;
        std::cout << "请输入要发送的数据:" << std::endl;
        std::getline(std::cin, send_buf);
        size_t n = sendto(_sockfd, send_buf.c_str(), send_buf.size(), 0, (struct sockaddr*)&server, sizeof(server));

        //发送完毕,接收服务器返回的数据
        if (n > 0)
        {
            struct sockaddr_in remote;
            socklen_t addr_len = sizeof(remote);
            char recv_buf[default_buf_size];


            int len = recvfrom(_sockfd, recv_buf, default_buf_size, 0, (struct sockaddr*)&remote, &addr_len);
            if (len < 0)
            {
                printf("recvfrom error:%d\n", errno);
                break;
            }
            else
            {
                recv_buf[len] = 0;
                printf("recvfrom success,data:%s\n", recv_buf);
            }
        }
        closesocket(_sockfd);

    }
    



    return 0;
}

运行之后输入127.0.0.1 8888

效果:

 

现在服务器端不止想知道客户端发来了什么,我还想知道客户端ip和端口号,并将其打印出来:

现在我想把获取解析客户端ip和port这一部分单独封装到一个类里放另一个文件,提供接口出来:

PortIp.hpp

#pragma once
#include"nocopy.hpp"
#include"winsock2.h"
#include"ws2tcpip.h"
#include<iostream>
#include<cstring>
#include <string>


class PortIp {
public:
    PortIp(struct sockaddr_in& client_addr )
    {
        _port = ntohs(client_addr.sin_port);
    _ip = inet_ntoa(client_addr.sin_addr);
        
    }
    std::string GetIp() { return _ip; }
    uint16_t GetPort() { return _port; }
    std::string Print()
    {
        std::string info = std::string(_ip) + ':' + std::to_string(_port);
        return info;
    }
    ~PortIp() {}
private:
    uint16_t _port;
        char* _ip;
};

 效果:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

孙鹏宇.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值