Chat room (win10 environment c implementation)

  1. Preparation for socket programming under windows

When programming sockets under Windows, you must call the WSAStartup function to set the WINsock version used by the program and initialize the corresponding version of the library.

#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. The framework
    is as shown below. The connection and information transfer between the client and the server are roughly as follows:

Zhihu-Xiao Lin coding
Server-side framework, regardless of data transfer, the entire process based on TCP is implemented as follows:
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 framework
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. achieve communication
#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. The chat room mainly runs the server, and then each client connects to the server to realize communication between each client. This involves the knowledge of multi-process. Since the cost of opening a process in Windows is much higher than that in Linux, it is usually recommended to have multiple threads in a single process in Windows, while in Linux it is multi-process and single thread, so there are more in Linux. For inter-process communication, what we want to learn now is the knowledge of opening multi-threads in Windows, which involves the CreateThread function.

windows creates thread

#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

There are two waiting functions to mention here, which cause the thread to hang and wait for the HANDLE object to have information.

#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被废弃了

The above function can only wait for a single object, and the operation is simple. For multiple objects, you can use the WaitForMultipleObjects function.

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

Create Event object

#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. Let’s connect the above preparation knowledge together

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);
}

Insert image description here
6. A little explanation:
The server starts waiting for the connection after the socket is bound. When there is a new connection request, a new thread is opened for the client to connect; the client has two threads responsible for sending information and receiving information respectively.
That's roughly it. There will be python version and go version in the future. Let's dig holes first.

Guess you like

Origin blog.csdn.net/weixin_44948269/article/details/118991783