网络编程——C++ Socket编程及TCP/UDP通信代码实现

一、简介

Socket编程的目的是使网络的进程进行通信,基于TCP/IP协议簇,通过三元组(ip地址、协议、端口)标志进程,并通过该标志与其他进行进行交互。使用TCP/IP协议的应用程序通常采用应用编程接口,套接字Socket是当前的主流通信方式,“一切皆可Socket”。
在这里插入图片描述
Socket通信流程图:
在这里插入图片描述

二、Socket是什么

网络的进程通过Socket进行通信,Socket本身起源于Unix,基于“一切皆文件”理论,通过“打开->读写->关闭”的模式进行操作,而Socket本身也是基于这种模式,可以把Socket理解成一种特殊的文件,而Socket函数就是对其进行操作,如打开、读、写、关闭等。
在这里插入图片描述

三、Socket的基本操作

1.socket()函数

#include <sys/socket.h>
int socket(int domain, int type, int protocol);

函数描述:创建一个socket描述符,唯一标志一个socket,用于后续的读写操作
参数解释
domain:协议域(协议簇),决定了socket的地址类型,常用的协议有AF_INET(IPV4互联网协议簇)、PF_INET6/AF_INET6(IPV6互联网协议簇)、AF_UNIX/AF_UNIX(要用一个绝对路径名作为地址)。
type:指socket类型,有面向连接的套接字(SOCK_STREAM)和面向消息的套接字(SOCK_DGRAM),其中面向连接的套接字可以理解成TCP协议,数据稳定、按序传输,不存在数据边界,且收发数据在套接字内部有缓冲,所以服务器和客户端进行I/O操作时并不会马上调用,可能分多次调用;面向消息的套接字可以看做UDP,特点:快速传输、有数据边界、数据可能丢失、传输数据大小受限。
protocol:指计算机间通信中使用的协议信息。一般都可以为0(当protocol为0时,会自动选择type类型对应的默认协议。),如果同一协议簇中存在多个数据传输方式相同的协议,则才用第三个参数。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等。

2.bind()函数

#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

函数描述:把一个地址族中的特定地址赋给socket(如果一个TCP客户或者服务器不用bind绑定一个端口,则调用connect或listen时,内核就要为相应的套接字选择一个临时端口(一般TCP客户端可这样做,TCP服务器不可,因为服务器的端口是大家熟知的))
参数解释
sockfd:指的是通过socket()创建的描述字,唯一标识一个socket。
addr:一个指针,指向要绑定的协议地址。

struct sockaddr_in {
    sa_family_t      sin_family;   //地址簇,取值AF_INET(IPV4),AF_INET6(IPV6)
    unit16_t         sin_port;     //16位TCP/UDP端口号,以网络字节序保存
    struct in_addr   sin_addr;     //32位IP地址,以网络字节序保存
    char             sin_zero[8];  //不使用,但一般初始化为0
}

struct in_addr {
    In_addr_t    s_addr;   //32位IP地址
}

addrlen:对应的是地址的长度。
注意
通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
相关知识

  • 主机字节序:有大端和小端模式
    (1)小端模式:低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
    (2)大端模式:高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
  • 网络字节序:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。
//short是16位,一般用于端口号,long一般用于地址
unsigned short htons(unsigned short);   //host to network把short类型数据
unsigned short ntohs(unsigned short);
unsigned long htonl(unsigned long);
unsigned long ntohl(unsigned long);

//还有两个可以把字符串形式的IP地址转换成32位整数型数据,成功调用后返回32位大端整数
in_addr_t inet_addr(const char * string);
int inet_aton(const char * string, struct in_addr * addr);
 
//iner_aton可以将转换后的IP地址信息带入sockaddr_in结构体中声明in_addr结构体变量。
char * inet_ntoa(struct in_addr adr);  //将网络字节序整数型IP地址转换成字符串
 
//以下两个函数ipv4和ipv6通用
//将点分十进制的ip地址转化为用于网络传输的数值格式
int inet_pton(int family, const char *strptr, void *addrptr);  
//将数值格式转化为点分十进制的ip地址格式
const char *inet_ntop(int family,const void *addrptr, char *strptr, size_t len);  

因此,在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是大端模式。

3.listen()函数

#include <sys/socket.h>
int listen(int sockfd, int backlog);

函数描述:listen将socket设置为被动监听类型,等待客户的连接请求。
参数解释
sockfd:要监听的socket描述字。
backlog:可以排队的最大连接个数。

4.connect()函数

#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

函数描述:客户端调用connect()来连接服务器,进行信息传输。
参数解释
sockfd:客户端的socket描述字。
addr:服务器的socket地址。
addrlen:socket地址的长度。

5.accept()函数

#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

函数描述:TCP服务器在依次调用socket()、bind()、listen()后,开始监听指定socket地址,接着调用accept()获取请求,建立连接;TCP客户端依次调用socket()、connect()就可以发送连接请求。
参数解释
sockfd:服务器的socket描述字。
addr:指向 struct sockaddr 的指针,用于返回客户端的协议地址。
addrlen:协议地址的长度。
返回值
连接成功,会返回由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。
注意
内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。

6.read()、write()函数

网络I/O操作有以上几组,推荐使用recvmsg() - sendmsg()。

read() - write()
recv() - send()
readv() - writev()
recvmsg() - sendmsg()
recvfrom() - sendto()
#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);


#include <sys/types.h>
#include <sys/socket.h>

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

函数描述:上述描述了几组I/O操作的函数
参数解释
read:负责从fd中读取内容.当读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题
write:将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数。失败时返回-1,并设置error变量。

  • write的返回值大于0,表示写了部分或者是全部的数据。
  • 返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接)。

7.close()函数

#include <unistd.h>
int close(int fd);

函数描述:数据传输结束时调用close()函数。close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。

三、Win10下VS2019实现TCP Socket通信demo(多线程)

1.环境配置

在win10环境下直接使用 #include <sys/socket.h> 会报错,所以需要改用 #include <WinSock2.h> 实现通信,配置过程如下:

  1. [右键]项目->属性->链接器->输入->附加依赖项->编辑->添加ws2_32.lib,取消勾选"从父级或项目默认设置继承"
    在这里插入图片描述
    在这里插入图片描述
  2. 属性->C/C+±>常规->SDL检查 设置为否
    在这里插入图片描述

配置结束后即可开始通信。

2.TCP服务器代码(多线程)

#include <stdio.h>
#include <WinSock2.h> //windows socket的头文件
#include <Windows.h>
#include <iostream>
#include <thread>
#include <mutex>
#include <process.h>
#pragma comment(lib, "ws2_32.lib") //连接winsock2.h的静态库文件
using namespace std;

//mutex 每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。
//同一时刻,只能有一个线程持有该锁。
mutex m;

//定义结构体用来设置
typedef struct my_file
{
	SOCKET clientSocket; //文件内部包含了一个SOCKET 用于和客户端进行通信
	sockaddr_in clientAddr; //用于保存客户端的socket地址
	int id; //文件块的序号
}F;

DWORD WINAPI transmmit(const LPVOID arg)
{
	//实际上这里为了追求并发性不应该加锁,上锁是为了方便看输出
	m.lock();

	F* temp = (F*)arg;
	//获取文件的序号
	//int file_id = temp->id;
	//获取客户机的端口号
	//ntohs(temp -> clientAddr.sin_port); 
	cout << "测试开始,等待客户端发送消息..." << endl;
	//从客户端处接受数据
	char Buffer[MAXBYTE] = { 0 }; //缓冲区
	recv(temp->clientSocket, Buffer, MAXBYTE, 0); //recv方法 从客户端通过clientScocket接收
	cout << "线程" << temp->id << "从客户端的" << ntohs(temp->clientAddr.sin_port) << "号端口收到:" << Buffer << endl;

	//发送简单的字符串到客户端
	const char* s = "Server file";
	send(temp->clientSocket, s, strlen(s) * sizeof(char) + 1, NULL);
	cout << "线程" << temp->id << "通过客户端的" << ntohs(temp->clientAddr.sin_port) << "号端口发送:" << s << endl;

	m.unlock();

	return 0;
}

int main()
{
	//加载winsock库,第一个参数是winsocket load的版本号(2.3)
	WSADATA wsaData;
	WSAStartup(MAKEWORD(2, 3), &wsaData);
	
	//创建服务器端的socket(协议族, sokcet类型)
	SOCKET servSocket = socket(AF_INET, SOCK_STREAM, 0);//如果改成SOCK_DGRAM则使用UDP
	
	// 初始化socket信息
	sockaddr_in servAddr; //服务器的socket地址,包含sin_addr表示IP地址,sin_port保持端口号和sin_zero填充字节
	memset(&servAddr, 0, sizeof(SOCKADDR)); //初始化socket地址
	
	//设置Socket的连接地址、方式和端口,并绑定
	servAddr.sin_family = PF_INET; //设置使用的协议族
	servAddr.sin_port = htons(2017); //设置使用的端口
	servAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //设置绑定的IP地址
	::bind(servSocket, (SOCKADDR*)&servAddr, sizeof(SOCKADDR)); //将之前创建的servSocket和端口,IP地址绑定

	HANDLE hThread[20]; //获取句柄
	listen(servSocket, 20); //监听服务器端口,最多20个连接
	for (int i = 0; i < 20; i++)
	{
		F* temp = new F; //创建新的传输结构体
		sockaddr_in clntAddr;
		int nSize = sizeof(SOCKADDR);
		SOCKET clientSock = accept(servSocket, (SOCKADDR*)&clntAddr, &nSize);
		//temp数据成员赋值
		temp->clientSocket = clientSock;
		temp->id = i + 1;
		temp->clientAddr = clntAddr;
		//通过句柄创建子线程
		hThread[i] = CreateThread(NULL, 0, &transmmit, temp, 0, NULL);
	}

	//等待子线程完成
	WaitForMultipleObjects(20, hThread, TRUE, INFINITE);
	cout << WSAGetLastError() << endl; //查看错误信息

	//关闭socket,释放winsock
	closesocket(servSocket);
	WSACleanup();

	cout << "服务器连接已关闭。" << endl;
	system("pause");

	return 0;
}

3.TCP客户端代码

#include <stdio.h>
#include <WinSock2.h> //windows socket的头文件
#include <Windows.h>
#include <iostream>
#include <thread>
#include <process.h>
#pragma comment(lib, "ws2_32.lib") //连接winsock2.h的静态库文件
using namespace std;

int main()
{
	//加载winsock库
	WSADATA wsadata;
	WSAStartup(MAKEWORD(2, 3), &wsadata);

	//客户端socket
	SOCKET clientSock = socket(PF_INET, SOCK_STREAM, 0);
	
	//初始化socket信息
	//memset:作用是在一段内存块中填充某个给定的值,它对较大的结构体或数组进行清零操作的一种最快方法。
	sockaddr_in clientAddr;
	memset(&clientAddr, 0, sizeof(SOCKADDR));
	
	//设置Socket的连接地址、方式和端口
	clientAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
	clientAddr.sin_family = PF_INET;
	clientAddr.sin_port = htons(2017);
	
	//建立连接
	connect(clientSock, (SOCKADDR*)&clientAddr, sizeof(SOCKADDR));
	cout << "已建立连接。" << endl;

	//发送消息
	char* s = new char[100];
	cout << "请输入你要发送的文字消息: ";
	cin >> s;
	send(clientSock, s, strlen(s) * sizeof(char) + 1, NULL);
	cout << "已发送:" << s << endl;

	//接收消息
	system("pause");
	char Buffer[MAXBYTE] = { 0 };
	recv(clientSock, Buffer, MAXBYTE, 0);
	cout << "通过端口:" << ntohs(clientAddr.sin_port) << "接收到:" << Buffer << endl;

	//关闭连接
	closesocket(clientSock);
	WSACleanup();
	cout << "客户端连接已关闭。" << endl;
	
	system("pause");
	return 0;
}

四、Win10下VS2019实现UDP Socket通信demo

1.环境配置

在win10环境下直接使用 #include <sys/socket.h> 会报错,所以需要改用 #include <WinSock2.h> 实现通信,配置过程如下:

  1. [右键]项目->属性->链接器->输入->附加依赖项->编辑->添加ws2_32.lib,取消勾选"从父级或项目默认设置继承"
    在这里插入图片描述
    在这里插入图片描述
  2. 属性->C/C+±>常规->SDL检查 设置为否
    在这里插入图片描述

配置结束后即可开始通信。

2.UDP服务器

#include <Winsock2.h>
#include <stdio.h>

void main()
{
	//加载套接字库
	WORD wVersionRequested;
	WSADATA wsaData;
	int err;

	wVersionRequested = MAKEWORD(1, 1);

	err = WSAStartup(wVersionRequested, &wsaData);//错误会返回WSASYSNOTREADY
	if (err != 0)
	{
		return;
	}

	if (LOBYTE(wsaData.wVersion) != 1 ||     //低字节为主版本
		HIBYTE(wsaData.wVersion) != 1)      //高字节为副版本
	{
		WSACleanup();
		return;
	}

	printf("server is operating!\n\n");
	//创建用于监听的套接字
	SOCKET sockSrv = socket(AF_INET, SOCK_DGRAM, 0);//失败会返回 INVALID_SOCKET
	//printf("Failed. Error Code : %d",WSAGetLastError())//显示错误信息

	SOCKADDR_IN addrSrv;     //定义sockSrv发送和接收数据包的地址
	addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
	addrSrv.sin_family = AF_INET;
	addrSrv.sin_port = htons(6000);

	//绑定套接字, 绑定到端口
	bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));//会返回一个SOCKET_ERROR
	//将套接字设为监听模式, 准备接收客户请求


	SOCKADDR_IN addrClient;   //用来接收客户端的地址信息
	int len = sizeof(SOCKADDR);
	char recvBuf[100];    //收
	char sendBuf[100];    //发
	char tempBuf[100];    //存储中间信息数据

	while (1)
	{

		//等待并数据
		recvfrom(sockSrv, recvBuf, 100, 0, (SOCKADDR*)&addrClient, &len);
		if ('q' == recvBuf[0])
		{
			sendto(sockSrv, "q", strlen("q") + 1, 0, (SOCKADDR*)&addrClient, len);
			printf("Chat end!\n");
			break;
		}
		sprintf_s(tempBuf, "%s say : %s", inet_ntoa(addrClient.sin_addr), recvBuf);
		printf("%s\n", tempBuf);

		//发送数据
		printf("Please input data: \n");
		gets_s(sendBuf);
		sendto(sockSrv, sendBuf, strlen(sendBuf) + 1, 0, (SOCKADDR*)&addrClient, len);
	}
	closesocket(sockSrv);
	WSACleanup();
}

3.UDP客户端

#include <Winsock2.h>
#include <stdio.h>

void main()
{
	//加载套接字库
	WORD wVersionRequested;
	WSADATA wsaData;
	int err;

	wVersionRequested = MAKEWORD(1, 1);

	err = WSAStartup(wVersionRequested, &wsaData);
	if (err != 0)
	{
		return;
	}

	if (LOBYTE(wsaData.wVersion) != 1 ||     //低字节为主版本
		HIBYTE(wsaData.wVersion) != 1)      //高字节为副版本
	{
		WSACleanup();
		return;
	}

	printf("Client is operating!\n\n");
	//创建用于监听的套接字
	SOCKET sockSrv = socket(AF_INET, SOCK_DGRAM, 0);

	sockaddr_in  addrSrv;
	addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//输入你想通信的她(此处是本机内部)
	addrSrv.sin_family = AF_INET;
	addrSrv.sin_port = htons(6000);


	int len = sizeof(SOCKADDR);

	char recvBuf[100];    //收
	char sendBuf[100];    //发
	char tempBuf[100];    //存储中间信息数据

	while (1)
	{

		printf("Please input data: \n");
		gets_s(sendBuf);
		sendto(sockSrv, sendBuf, strlen(sendBuf) + 1, 0, (SOCKADDR*)&addrSrv, len);
		//等待并数据
		recvfrom(sockSrv, recvBuf, 100, 0, (SOCKADDR*)&addrSrv, &len);

		if ('q' == recvBuf[0])
		{
			sendto(sockSrv, "q", strlen("q") + 1, 0, (SOCKADDR*)&addrSrv, len);
			printf("Chat end!\n");
			break;
		}
		sprintf_s(tempBuf, "%s say : %s", inet_ntoa(addrSrv.sin_addr), recvBuf);
		printf("%s\n", tempBuf);

		//发送数据

	}
	closesocket(sockSrv);
	WSACleanup();
}

五、参考文献

1. Linux Socket编程(不限Linux)
2. 网络编程基础知识
3. Visual Studio 2019 C++实现socket通信,添加ws2_32.lib库
4.Windows环境下C++多线程TCP通信
5. C++ udp实现简单的通信

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值