socket多人聊天程序C语言版(一)

2025-05-27 0 23

首先,不要一步登天直接解决多人聊天这个问题,先把问题化简。
1.多人聊天的核心问题是服务器如何标识不同的客户端,如何根据客户端的需求转发消息给指定客户端。
2.多人聊天转化为C-C聊天,但是不再是直接C-C,而是通过server转发消息,所以变成==>C-S-C。
3.server如何允许2个client同时连接,设置listen函数的第二个参数,最大连接数。
4.server如何标识两个client,用一个结构体数组来存放两个client的信息。
5.server如何转发消息给client,很简单,先接收到的发送给还没接收到的。如图:

socket多人聊天程序C语言版(一)

6.server如何管理两个client的连接状态,连接成功很简单,就是accpet成功后就是连接成功了。但是怎么判断连接断开呢?这个涉及到的select函数的使用,有点复杂~,所以我就简单的用了一个send函数发送一个空消息来判断是否断开连接,这个不严谨,容易出BUG,但是实践起来简单就使用了它。
7.要用线程来管理接收消息、发送消息、接受请求、管理连接状态。

技术要点:C语言线程函数的使用。

?

1

2

3

4

5

6

7

8

9

10
_beginthreadex函数原型

_ACRTIMP uintptr_t __cdecl _beginthreadex

( _In_opt_ void* _Security,//安全属性,NULL为默认安全属性

_In_ unsigned _StackSize,//线程堆栈的大小。如果为0,则线程堆栈大小和创建它的线程的相同。一般用0

_In_ _beginthreadex_proc_type _StartAddress, //线程函数的地址

_In_opt_ void* _ArgList, //传进线程的函数

_In_ unsigned _InitFlag, //线程初始状态,0:立即运行;CREATE_SUSPEND:悬挂(如果出事状态定义为悬挂,就要调用ResumeThread(HANDLE) 来激活线程的运行)

_Out_opt_ unsigned* _ThrdAddr //用于记录线程ID的地址

)

例子:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15
#include <process.h>

#include <stdio.h>

unsigned __stdcall Thread(void* param)

{

printf("%d\\n", *(int*)param); //这里必须先要强行转换为int*,不然void* 直接解引用会出错。

return 0;

}

int main()

{

int i = 0;

_beginthreadex(NULL, 0, Thread, &i, 0, NULL);

return 0;

}

1V1,C-S-C聊天例子:
编写环境:win10,VS2015

效果图:

socket多人聊天程序C语言版(一)

socket多人聊天程序C语言版(一)

socket多人聊天程序C语言版(一)

server code:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239
#include <WinSock2.h>

#include <process.h>

#include <stdio.h>

#include <stdlib.h>

#pragma comment(lib,"ws2_32.lib")

#define SEND_OVER 1 //已经转发消息

#define SEND_YET 0 //还没转发消息

int g_iStatus = SEND_YET;

SOCKET g_ServerSocket = INVALID_SOCKET; //服务端套接字

SOCKADDR_IN g_ClientAddr = { 0 }; //客户端地址

int g_iClientAddrLen = sizeof(g_ClientAddr);

bool g_bCheckConnect = false; //检查连接情况

HANDLE g_hRecv1 = NULL;

HANDLE g_hRecv2 = NULL;

//客户端信息结构体

typedef struct _Client

{

SOCKET sClient; //客户端套接字

char buf[128]; //数据缓冲区

char userName[16]; //客户端用户名

char IP[20]; //客户端IP

UINT_PTR flag; //标记客户端,用来区分不同的客户端

}Client;

Client g_Client[2] = { 0 }; //创建一个客户端结构体

//发送数据线程

unsigned __stdcall ThreadSend(void* param)

{

int ret = 0;

int flag = *(int*)param;

SOCKET client = INVALID_SOCKET; //创建一个临时套接字来存放要转发的客户端套接字

char temp[128] = { 0 }; //创建一个临时的数据缓冲区,用来存放接收到的数据

memcpy(temp, g_Client[!flag].buf, sizeof(temp));

sprintf(g_Client[flag].buf, "%s: %s", g_Client[!flag].userName, temp);//添加一个用户名头

if (strlen(temp) != 0 && g_iStatus == SEND_YET) //如果数据不为空且还没转发则转发

ret = send(g_Client[flag].sClient, g_Client[flag].buf, sizeof(g_Client[flag].buf), 0);

if (ret == SOCKET_ERROR)

return 1;

g_iStatus = SEND_OVER; //转发成功后设置状态为已转发

return 0;

}

//接受数据

unsigned __stdcall ThreadRecv(void* param)

{

SOCKET client = INVALID_SOCKET;

int flag = 0;

if (*(int*)param == g_Client[0].flag) //判断是哪个客户端发来的消息

{

client = g_Client[0].sClient;

flag = 0;

}

else if (*(int*)param == g_Client[1].flag)

{

client = g_Client[1].sClient;

flag = 1;

}

char temp[128] = { 0 }; //临时数据缓冲区

while (1)

{

memset(temp, 0, sizeof(temp));

int ret = recv(client, temp, sizeof(temp), 0); //接收数据

if (ret == SOCKET_ERROR)

continue;

g_iStatus = SEND_YET; //设置转发状态为未转发

flag = client == g_Client[0].sClient ? 1 : 0; //这个要设置,否则会出现自己给自己发消息的BUG

memcpy(g_Client[!flag].buf, temp, sizeof(g_Client[!flag].buf));

_beginthreadex(NULL, 0, ThreadSend, &flag, 0, NULL); //开启一个转发线程,flag标记着要转发给哪个客户端

//这里也可能是导致CPU使用率上升的原因。

}

return 0;

}

//管理连接

unsigned __stdcall ThreadManager(void* param)

{

while (1)

{

if (send(g_Client[0].sClient, "", sizeof(""), 0) == SOCKET_ERROR)

{

if (g_Client[0].sClient != 0)

{

CloseHandle(g_hRecv1); //这里关闭了线程句柄,但是测试结果断开连C/S接后CPU仍然疯涨

CloseHandle(g_hRecv2);

printf("Disconnect from IP: %s,UserName: %s\\n", g_Client[0].IP, g_Client[0].userName);

closesocket(g_Client[0].sClient); //这里简单的判断:若发送消息失败,则认为连接中断(其原因有多种),关闭该套接字

g_Client[0] = { 0 };

}

}

if (send(g_Client[1].sClient, "", sizeof(""), 0) == SOCKET_ERROR)

{

if (g_Client[1].sClient != 0)

{

CloseHandle(g_hRecv1);

CloseHandle(g_hRecv2);

printf("Disconnect from IP: %s,UserName: %s\\n", g_Client[1].IP, g_Client[1].userName);

closesocket(g_Client[1].sClient);

g_Client[1] = { 0 };

}

}

Sleep(2000); //2s检查一次

}

return 0;

}

//接受请求

unsigned __stdcall ThreadAccept(void* param)

{

int i = 0;

int temp1 = 0, temp2 = 0;

_beginthreadex(NULL, 0, ThreadManager, NULL, 0, NULL);

while (1)

{

while (i < 2)

{

if (g_Client[i].flag != 0)

{

++i;

continue;

}

//如果有客户端申请连接就接受连接

if ((g_Client[i].sClient = accept(g_ServerSocket, (SOCKADDR*)&g_ClientAddr, &g_iClientAddrLen)) == INVALID_SOCKET)

{

printf("accept failed with error code: %d\\n", WSAGetLastError());

closesocket(g_ServerSocket);

WSACleanup();

return -1;

}

recv(g_Client[i].sClient, g_Client[i].userName, sizeof(g_Client[i].userName), 0); //接收用户名

printf("Successfuuly got a connection from IP:%s ,Port: %d,UerName: %s\\n",

inet_ntoa(g_ClientAddr.sin_addr), htons(g_ClientAddr.sin_port), g_Client[i].userName);

memcpy(g_Client[i].IP, inet_ntoa(g_ClientAddr.sin_addr), sizeof(g_Client[i].IP)); //记录客户端IP

g_Client[i].flag = g_Client[i].sClient; //不同的socke有不同UINT_PTR类型的数字来标识

i++;

}

i = 0;

if (g_Client[0].flag != 0 && g_Client[1].flag != 0) //当两个用户都连接上服务器后才进行消息转发

{

if (g_Client[0].flag != temp1) //每次断开一个连接后再次连上会新开一个线程,导致cpu使用率上升,所以要关掉旧的

{

if (g_hRecv1) //这里关闭了线程句柄,但是测试结果断开连C/S接后CPU仍然疯涨

CloseHandle(g_hRecv1);

g_hRecv1 = (HANDLE)_beginthreadex(NULL, 0, ThreadRecv, &g_Client[0].flag, 0, NULL); //开启2个接收消息的线程

}

if (g_Client[1].flag != temp2)

{

if (g_hRecv2)

CloseHandle(g_hRecv2);

g_hRecv2 = (HANDLE)_beginthreadex(NULL, 0, ThreadRecv, &g_Client[1].flag, 0, NULL);

}

}

temp1 = g_Client[0].flag; //防止ThreadRecv线程多次开启

temp2 = g_Client[1].flag;

Sleep(3000);

}

return 0;

}

//启动服务器

int StartServer()

{

//存放套接字信息的结构

WSADATA wsaData = { 0 };

SOCKADDR_IN ServerAddr = { 0 }; //服务端地址

USHORT uPort = 18000; //服务器监听端口

//初始化套接字

if (WSAStartup(MAKEWORD(2, 2), &wsaData))

{

printf("WSAStartup failed with error code: %d\\n", WSAGetLastError());

return -1;

}

//判断版本

if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)

{

printf("wVersion was not 2.2\\n");

return -1;

}

//创建套接字

g_ServerSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

if (g_ServerSocket == INVALID_SOCKET)

{

printf("socket failed with error code: %d\\n", WSAGetLastError());

return -1;

}

//设置服务器地址

ServerAddr.sin_family = AF_INET;//连接方式

ServerAddr.sin_port = htons(uPort);//服务器监听端口

ServerAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);//任何客户端都能连接这个服务器

//绑定服务器

if (SOCKET_ERROR == bind(g_ServerSocket, (SOCKADDR*)&ServerAddr, sizeof(ServerAddr)))

{

printf("bind failed with error code: %d\\n", WSAGetLastError());

closesocket(g_ServerSocket);

return -1;

}

//设置监听客户端连接数

if (SOCKET_ERROR == listen(g_ServerSocket, 20000))

{

printf("listen failed with error code: %d\\n", WSAGetLastError());

closesocket(g_ServerSocket);

WSACleanup();

return -1;

}

_beginthreadex(NULL, 0, ThreadAccept, NULL, 0, 0);

for (int k = 0;k < 100;k++) //让主线程休眠,不让它关闭TCP连接.

Sleep(10000000);

//关闭套接字

for (int j = 0;j < 2;j++)

{

if (g_Client[j].sClient != INVALID_SOCKET)

closesocket(g_Client[j].sClient);

}

closesocket(g_ServerSocket);

WSACleanup();

return 0;

}

int main()

{

StartServer(); //启动服务器

return 0;

}

client code:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119
#define _WINSOCK_DEPRECATED_NO_WARNINGS

#include <WinSock2.h>

#include <process.h>

#include <stdio.h>

#include <stdlib.h>

#include <conio.h>

#pragma comment(lib,"ws2_32.lib")

#define RECV_OVER 1

#define RECV_YET 0

char userName[16] = { 0 };

int iStatus = RECV_YET;

//接受数据

unsigned __stdcall ThreadRecv(void* param)

{

char buf[128] = { 0 };

while (1)

{

int ret = recv(*(SOCKET*)param, buf, sizeof(buf), 0);

if (ret == SOCKET_ERROR)

{

Sleep(500);

continue;

}

if (strlen(buf) != 0)

{

printf("%s\\n", buf);

iStatus = RECV_OVER;

}

else

Sleep(100);

}

return 0;

}

//发送数据

unsigned __stdcall ThreadSend(void* param)

{

char buf[128] = { 0 };

int ret = 0;

while (1)

{

int c = getch();

if(c == 72 || c == 0 || c == 68)//为了显示美观,加一个无回显的读取字符函数

continue; //getch返回值我是经过实验得出如果是返回这几个值,则getch就会自动跳过,具体我也不懂。

printf("%s: ", userName);

gets_s(buf);

ret = send(*(SOCKET*)param, buf, sizeof(buf), 0);

if (ret == SOCKET_ERROR)

return 1;

}

return 0;

}

//连接服务器

int ConnectServer()

{

WSADATA wsaData = { 0 };//存放套接字信息

SOCKET ClientSocket = INVALID_SOCKET;//客户端套接字

SOCKADDR_IN ServerAddr = { 0 };//服务端地址

USHORT uPort = 18000;//服务端端口

//初始化套接字

if (WSAStartup(MAKEWORD(2, 2), &wsaData))

{

printf("WSAStartup failed with error code: %d\\n", WSAGetLastError());

return -1;

}

//判断套接字版本

if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)

{

printf("wVersion was not 2.2\\n");

return -1;

}

//创建套接字

ClientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

if (ClientSocket == INVALID_SOCKET)

{

printf("socket failed with error code: %d\\n", WSAGetLastError());

return -1;

}

//输入服务器IP

printf("Please input server IP:");

char IP[32] = { 0 };

gets_s(IP);

//设置服务器地址

ServerAddr.sin_family = AF_INET;

ServerAddr.sin_port = htons(uPort);//服务器端口

ServerAddr.sin_addr.S_un.S_addr = inet_addr(IP);//服务器地址

printf("connecting......\\n");

//连接服务器

if (SOCKET_ERROR == connect(ClientSocket, (SOCKADDR*)&ServerAddr, sizeof(ServerAddr)))

{

printf("connect failed with error code: %d\\n", WSAGetLastError());

closesocket(ClientSocket);

WSACleanup();

return -1;

}

printf("Connecting server successfully IP:%s Port:%d\\n",

IP, htons(ServerAddr.sin_port));

printf("Please input your UserName: ");

gets_s(userName);

send(ClientSocket, userName, sizeof(userName), 0);

printf("\\n\\n");

_beginthreadex(NULL, 0, ThreadRecv, &ClientSocket, 0, NULL); //启动接收和发送消息线程

_beginthreadex(NULL, 0, ThreadSend, &ClientSocket, 0, NULL);

for (int k = 0;k < 1000;k++)

Sleep(10000000);

closesocket(ClientSocket);

WSACleanup();

return 0;

}

int main()

{

ConnectServer(); //连接服务器

return 0;

}

这程序还有一些BUG,其中最大的就是关掉一个连接后CPU使用率疯涨,我测试过我想到的可能,还是找不到结果~,希望有大神懂的告知一下。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持快网idc。

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。

快网idc优惠网 建站教程 socket多人聊天程序C语言版(一) https://www.kuaiidc.com/74468.html

相关文章

发表评论
暂无评论