聊天室(win10环境c实现)

  1. windows下socket编程准备

window下socket编程,必须调用WSAStartup函数,来设置程序用到的WINsock版本并初始化相应版本的库

#include<winsock2.h>
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
//成功时返回0,失败返回非零的错误代码
//wVersionRequested,Winsock版本;lpWSAData,WSADATA结构体变量的地址值
int WSACleanup(void);
//成功返回0,失败返回SOCKET_ERROR
//调用该函数后,Winsock库归还windows系统,无法再调用winsock函数
#include<winsock2.h>
SOCKET socket(int af, int type, int protocol);
//socket函数本身返回整型数据,定义SOCKET类型为整型数据是为了以后的扩展
//af,套接字中使用协议族,常用有PF_INET(IPv4协议族),PF_INET6(IPv6协议族)
//type,套接字数据传输类型,有基于TCP的(SOCK_STREAM)和基于UDP的(SOCK_DGRAM)
//protocol,计算机间通信使用的协议信息,常用0
//成功则返回套接字句柄,失败返回INVALID_SOCKET,实际值为-1

int bind(SOCKET s, const struct SOCKADDR* name, int namelen);
//bind函数包含s(套接字),name(SOCKADDR_IN结构体地址变量),namelen(name参数长度)
//成功返回0,失败返回SOCKET_ERROR
//为s套接字分配IP地址和端口号等信息

int listen(SOCKET s, int backlog);
//成功时返回0, 失败返回SOCKET_ERROR
//backlog, 连接请求等待队列的长度,就像办理业务,套接字是窗口,每次只有一个人办业务,其他的只能排队等候

SOCKET accept(SOCKET s, struct SOCKADDR* addr, int addrlen);
//成功时返回套接字句柄,失败时返回INVALID_SOCKET
//addr,保存发起连接请求的客户端地址信息的变量地址值
//受理请求等待队列中待处理的客户端连接请求,调用成功将产生用于数据I/O的套接字并返回其文件操作符
//这个套接字是自动创建的,并自动与发起客户端建立连接
//前面创建的是服务端套接字,可以看成是接受连接请求的门卫;
//后面创建的是客户端套接字,用于传输数据

int connect(SOCKET s, const struct SOCKADDR* name, int namelen);
//成功返回0,失败返回SOCKET_ERROR
//用于从客户端发送连接请求
//name,保存目标服务端地址信息的变量地址值

int closesocket(SOCKET s);
//成功时返回0,失败返回SOCKET_ERROR
  1. 框架
    如下图,客户端和服务端的连接和信息传递大致如下:

知乎-小林coding
服务端框架,抛开数据传递,基于TCP的整个流程实现如下:
server.c

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<winsock2.h>
//windows下socket编程除了要导入winsock2.h头文件还需要链接ws2_32.lib
#pragma comment(lib,"Ws2_32.lib")
//错误处理函数,到了后面可以进行更改替换
void ErrorHandling(char *message);

int main(int argc, char *argv[]){
	SOCKET servSock, clntSock;
	SOCKADDR_IN servAddr, clntAddr;
	int clnt_addr_size;
	//Winsock初始化
	WSADATA wsaData;
	if(WSAStartup(MAKEWORD(2, 2), &wsaData)!=0)
		ErrorHandling("WSAStartup() error.");
	if(argc!=2){
		printf("Usage: %s <port>\n",argv[0]);
		return 0;
	}
	//socket创建服务端套接字
	servSock=socket(PF_INET, SOCK_STREAM, 0);
	if(servSock==INVALID_SOCKET)
		ErrorHandling("socket() error.");
	//servAddr结构体初始化
	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family=AF_INET;								//地址族信息初始化为AF_INET
	servAddr.sin_addr.s_addr=htonl(INADDR_ANY);					//IP地址初始化为主机,htonl函数将数据转换成网络字节序
	servAddr.sin_port=htons(atoi(argv[1]));						//端口号初始化为传参列表中紧跟在可执行文件后第一个参数,htons同样将数据转换成网络字节序

	if(bind(servSock, (SOCKADDR*)&servAddr, sizeof(servAddr))==SOCKET_ERROR)
		ErrorHandling("bind() error.");
	if(listen(servSock, 5)==SOCKET_ERROR)
		ErrorHandling("listen() error.");

	clnt_addr_size=sizeof(clntAddr);
	//处理连接请求,自动创建套接字连接客户端
	clntSock=accept(servSock, (SOCKADDR*)&clntAddr, &clnt_addr_size);
	if(clntSock==INVALID_SOCKET)
		ErrorHandling("accept() error.");
	//关闭套接字和注销winsock库
	closesocket(clntSock);
	closesocket(servSock);
	WSACleanup();
	return 0;
}
void ErrorHandling(char *message){
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

客户端框架
client.c

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<winsock2.h>
//windows下socket编程除了要导入winsock2.h头文件还需要链接ws2_32.lib
#pragma comment(lib,"Ws2_32.lib")
//错误处理函数,到了后面可以进行更改替换
void ErrorHandling(char *message);

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

	SOCKET sock;
	SOCKADDR_IN servAddr;
	int addr_size;
	
	//Winsock初始化
	WSADATA wsaData;
	if(WSAStartup(MAKEWORD(2, 2), &wsaData)!=0)
		ErrorHandling("WSAStartup() error.");
	if(argc!=3){
		printf("Usage: %s <IP> <port>\n",argv[0]);
		return 0;
	}
	//socket创建服务端套接字
	sock=socket(PF_INET, SOCK_STREAM, 0);
	if(sock==INVALID_SOCKET)
		ErrorHandling("socket() error.");
	
	//servAddr结构体初始化
	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family=AF_INET;								//地址族信息初始化为AF_INET
	servAddr.sin_addr.s_addr=inet_addr(argv[1]);				//IP地址初始化为传参列表第二个参数,inet_addr函数将点分十进制格式字符串转换成32位整形数据并返回
	servAddr.sin_port=atoi(argv[2]);							//端口号初始化为传参列表中紧跟在可执行文件后第三个参数,atoi函数转换字符串成整数

	//发起连接请求
	if(connect(sock, (SOCKADDR*)&servAddr, sizeof(servAddr))==SOCKET_ERROR)
		ErrorHandling("connect() error.");

	addr_size=sizeof(clntAddr);
	//处理连接请求,自动创建套接字连接客户端
	clntSock=accept(servSock, (SOCKADDR*)&clntAddr, &clnt_addr_size);
	if(clntSock==INVALID_SOCKET)
		ErrorHandling("accept() error.");
	//关闭套接字和注销winsock库
	closesocket(sock);
	WSACleanup();
	return 0;
}
void ErrorHandling(char *message){
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}
  1. 实现通信
#include<winsock2.h>
int send(SOCKET s, const char* buf, int len, int flags);
//成功时返回传输字节数,失败时返回SOCKET_ERROR
//s(套接字),buf(保存带传输数据的缓冲地址值),len(传输字节数),flags(传输数据时用到的多种选项信息,通常用0表示即可)
//Winsock数据传输函数

int recv(SOCKET s, const char* buf, int len, int flags);
//Winsock数据接收函数
  1. 聊天室主要是运行服务端,然后各个客户端连接到服务端后,实现各个客户端通信。这里就涉及到了多进程的知识了,由于windows中开进程的花销远比linux的要大,所以通常windows都是推荐单进程内多线程,linux则是多进程单线程,所以linux中更多进程间通信,我们现在要学习的是windows中开多线程的知识,这个就涉及到CreateThread函数了。

windows创建线程

#include<Windows.h>
HANDLE CreateThread(
	LPSECURITY_ATTRIBUTES	lpThreadAttributes,
	SIZE_T					dwStackSize,
	LPTHREAD_START_ROUTINE	lpStartAddress,
	LPVOID					lpParameter,
	DWORD					dwCreationFlags,
	LPDWORD					lpThreadId,
)
//lpThreadAttributes,线程的安全属性,一般设置为NULL
//dwStackSize,线程栈空间大小(单位:字节),一般设置为0即默认大小
//lpStartAddress,线程函数(为函数指针类型),指定的调用形式为:DWORD WINAPI funname(LPVOID lpThreadParameter);
//lpParameter,传给上面线程函数的参数,实际上都是void*类型
//dwCreationFlags,32位无符号整型,通常为0表示创建好线程立即启动运行
//lpThreadId,线程创建成功返回的线程ID,32位无符号整型(DWORD)指针(LPDWORD)
//成功时返回标识/句柄(HANDLE),否则返回NULL

这里还要提一下两个等待函数,使得线程挂起,等待HANDLE对象有信

#include<Windows.h>
DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);
//参数hHandle为需要检测信号需要等待的内核对象,参数dwMilliseconds为等待内核对象的最大时间(单位:毫秒)
//函数返回类型为DWORD,无符号长整型,当dwMilliseconds设为INFINITE宏即表示无限等待下去
//等待信号的内核对象分别可以是线程、进程、Event(事件)、Mutex(互斥体)和Semaphore(信号量)
//当函数返回类型为WAIT_FAILED时表示调用失败可通过GetLastError函数得到错误码
//当返回WAIT_OBJECT_0时,表示函数成功等待到内核对象;WAIT_TIMEOUT表示等待超时;WAIT_ABANDONED表示等待的是Mutex并且持有该对象的线程结束运行当并没有调用ReleaseMutex函数释放持有权因此处于废弃状态,即该内核对象Mutex被废弃了

上面这个函数是只能等待单个对象,操作也简单,对于多个对象可以使用WaitForMultipleObjects函数

#include<Windows.h>
DWORD WaitForMultipleObjects(
	DWORD			nCount,
	const HANDLE	*lpHandles,
	BOOL			bWaitAll,
	DWORD			dwMilliseconds
);
//lpHandles是需要等待的对象数组指针,nCount指定数组长度,bWaitAll表示是否等待数组lpHandles中所有对象都有信号,dwMilliseconds即等待时间

创建Event对象

#include<Windows.h>
HANDLE CreateEvent(
	LPSECURITY_ATTRIBUTES	lpEventAttributes,
	BOOL					bManualReset,
	BOOL					bInitialState,
	LPCTSTR					lpName
);
//lpEventAttributes为Event对象的安全属性,一般设为NULL即默认的安全属性
//bManualReset设置Event对象为有信号状态(受信)时的行为,设置为TRUE需要手动调用ResetEvent函数将Event重置为无信号状态;设置为FALSE时Event事件对象会在受信后自动重置为无信号状态
//bInitialState,设置Event事件对象的初始状态是否受信,TRUE为有信号,FALSE为无信号
//lpName,设置Event对象的名称,不需要设置时设为NULL;Event对象可通过名称在不同进程间共享
//成功创建Event对象时返回句柄,失败返回NULL

BOOL SetEvent(HANDLE hEvent);
//将hEvent设置为我们需要设置的Event句柄
BOOL ResetEvent(HANDLE hEvent);
//重置hEvent句柄对象为无信号状态
  1. 让我们把上面的准备知识串联起来

chatroom_server.c

#include<stdio.h>
#include<stdlib.h>
#include<winsock2.h>
#include<windows.h>
#define MAX 256
#define BUF 100
#pragma comment(lib, "Ws2_32.lib")

void ErrorHandling(char *message);					//错误处理函数
DWORD WINAPI msgHandle(LPVOID lp);					//线程执行函数
void sendMsg(char *message, int len);				//消息发送函数
HANDLE event;										//事件内核对象
int sockCount=0;									//统计套接字数量
int socks[MAX];										//管理套接字
HANDLE threads[MAX];								//管理线程

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

	//线程ID
	DWORD Id;
	SOCKET servSock, clntSock;
	int addrSize, i;
	SOCKADDR_IN servAddr,clntAddr;

	//初始化Ws2_32.lib
	WSADATA wsaData;
	if(WSAStartup(MAKEWORD(2,2), &wsaData)!=0)
		ErrorHandling("WSAStartup() error.");
	if(argc!=2){
		printf("Usage: %s <port>\n",argv[0]);
		exit(1);
	}
	
	//创建自动重置的、受信的事件内核对象
	event = CreateEvent(NULL, FALSE, TRUE, NULL);
	//创建套接字
	servSock = socket(AF_INET, SOCK_STREAM, 0);
	if(servSock == INVALID_SOCKET)
		ErrorHandling("socket() error.");

	//初始化SOCKADDR_IN结构体
	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family=AF_INET;
	servAddr.sin_addr.s_addr=htonl(INADDR_ANY);
	servAddr.sin_port=htons(atoi(argv[1]));

	//分配IP地址和端口号给套接字
	if(bind(servSock, (SOCKADDR*)&servAddr, sizeof(servAddr))==SOCKET_ERROR)
		ErrorHandling("bind() error.");
	if(listen(servSock, 5)==SOCKET_ERROR)
		ErrorHandling("listen() error.");
	printf("Start to listen.");
	
	addrSize=sizeof(clntAddr);
	while(1){
		printf("等待新连接\n");
		clntSock = accept(servSock, (SOCKADDR*)&clntAddr, &addrSize);
		if(clntSock == INVALID_SOCKET){
			printf("连接失败,重连");
			continue;
		}
		WaitForSingleObject(event, INFINITE);
		threads[sockCount] = CreateThread(
			NULL,				//默认安全属性
			0,					//默认堆栈大小
			msgHandle,			//执行线程的函数
			(void*)&clntSock,	//传给函数的参数
			0,					//指定线程立即运行
			&Id					//返回线程ID
			);
		if(threads == NULL)
			ErrorHandling("Failed to create thread.");
		socks[sockCount++] = clntSock;
		//设置受信
		SetEvent(event);
		printf("接收到一个连接:%s,执行线程ID: %d\r\n", inet_ntoa(clntAddr.sin_addr), Id);
	}
	WaitForMultipleObjects(sockCount, threads, 1, INFINITE);
	for(i=0;i < sockCount; i++)
		CloseHandle(threads[i]);
	closesocket(servSock);
	WSACleanup();
	return 0;
}

void sendMsg(char *message, int len){
	int i;
	//线程一直挂起直到HANDLE句柄所指对象有信号
	WaitForSingleObject(event, INFINITE);
	for(i = 0;i < sockCount; i++)
		send(socks[i], message, len, 0);
	//设置HANDLE句柄对象为发信号状态
	SetEvent(event);
}

//返回类型为DWORD,WINAPI是个宏,是函数调用形式,最终是_stdcall形式
DWORD WINAPI msgHandle(LPVOID lp){
	int clntSock = *((int*)lp);
	int strLen = 0, i;
	char message[BUF];

	while((strLen = recv(clntSock, message, sizeof(message), 0)) != -1){
		sendMsg(message, strLen);
		printf("发送成功\n");
	}
	//显示当前线程唯一标识符
	printf("客户端退出:%d\n", GetCurrentThreadId());
	WaitForSingleObject(event, INFINITE);
	for(i = 0;i < sockCount; i++){
		if(clntSock == socks[i]){
			while(i++ < sockCount - 1)
				socks[i] = socks[i + 1];
			break;
		}
	}
	sockCount--;
	//设置hEvent句柄受信
	SetEvent(event);
	closesocket(clntSock);
	return 0;
}

void ErrorHandling(char *message){
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

chatroom_client.c

#include<stdio.h>
#include<stdlib.h>
#include<winsock2.h>
#include<Windows.h>
#pragma comment(lib, "Ws2_32.lib")
#define BUF 256
#define SIZE 30

DWORD WINAPI sendmsg(LPVOID lp);					//发送信息函数
DWORD WINAPI recvmsg(LPVOID lp);					//接受信息函数
void ErrorHandling(char *message);					//错误处理函数

char name[SIZE]="";
char message[BUF];

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

	SOCKET sock;
	SOCKADDR_IN servAddr;
	HANDLE threads[2];
	DWORD Id;

	WSADATA wsaData;
	if(WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		ErrorHandling("WSAStartup() error.");
	if(argc!=3){
		printf("Usage: %s <IP> <port>\n",argv[0]);
		exit(1);
	}
	
	printf("输入你的名字:");
	scanf("%s", name);
	getchar();								//接收缓冲中的换行符
	
	//创建TCP套接字
	sock = socket(PF_INET, SOCK_STREAM, 0);
	if(sock == INVALID_SOCKET)
		ErrorHandling("socket() error.");
	//初始化SOCKADDR_IN结构体
	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.s_addr = inet_addr(argv[1]);
	servAddr.sin_port = (atoi(argv[2]));

	//发起连接请求
	if(connect(sock, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
		ErrorHandling("connect() error.");
	printf("连接成功\n");
	threads[0] = CreateThread(
		NULL,
		0,
		sendmsg,
		&sock,
		0,
		&Id
		);
	threads[1] = CreateThread(
		NULL,
		0,
		recvmsg,
		&sock,
		0,
		&Id
		);
	WaitForMultipleObjects(2, threads, 1, INFINITE);
	//关闭线程
	CloseHandle(threads[0]);
	CloseHandle(threads[1]);
	printf("Over.Press anykey to end.\n");
	getchar();
	closesocket(sock);
	WSACleanup();
	return 0;
}

DWORD WINAPI sendmsg(LPVOID lp){
	int sock = *((int*)lp);
	int rec;
	char msg[SIZE + BUF];
	while(1){
		fgets(message, BUF, stdin);
		if(!strcmp(message, "q\n")||!strcmp(message, "Q\n")){
			closesocket(sock);
			exit(0);
		}
		sprintf(msg, "[%s]: %s", name, message);
		rec=send(sock, msg, strlen(msg), 0);
	}
	return 0;
}

DWORD WINAPI recvmsg(LPVOID lp){
	int sock = *((int*)lp);
	char msg[SIZE + BUF];
	int strLen;
	while(1){
		strLen = recv(sock, msg, SIZE + BUF - 1, 0);
		if(strLen == -1)
			return -1;
		msg[strLen] = 0;
		fputs(msg, stdout);
	}
	return 0;
}

void ErrorHandling(char *message){
	fputs(message,stderr);
	fputc('\n', stderr);
	exit(1);
}

在这里插入图片描述
6.稍微讲解
服务器在套接字绑定后开始等待连接,有一个新的连接请求就新开一个线程给客户端连接;客户端则是两个线程分别负责发信息和接受信息。
大致就这样,后续还有python版本和go版本,先挖坑。

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 13
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值