post on 19 Mar 2019 about 5564words require 19min
CC BY 4.0 (除特别声明或转载文章外)
如果这篇博客帮助到你,可以请我喝一杯咖啡~
Chat 实验
掌握套节字的多线程编程方法。
利用客户/服务器(Client/Sever 或 CS)模式实现一个多人聊天(群聊)程序。其功能是每个客户发送给服务器的消息都会传送给所有的客户端。
flowchart TB
服务器--Hello-->A
A==Hello==>服务器
服务器--Hello-->B
服务器--Hello-->C
服务器--Hello-->D
这里我使用的环境是 Windows 10 + VSCode + gcc version 8.1.0 (x86_64-posix-sjlj-rev0, Built by MinGW-W64 project)
先阅读课件「套接字并发编程.PDF」。重点是读懂课件中「chat 并发编程(服务器)」和「chat 并发编程(客户端)」的流程图。 然后,完成下面步骤(截屏要同时显示服务器和至少两个客户端):
客户端程序:
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
#include <stdio.h>
#include <string.h>
#include <process.h>
#include <winsock2.h>
#define BUFLEN 2000
#define WSVERS MAKEWORD(2, 0)
#pragma comment(lib, "ws2_32.lib")
int finish = 0;
unsigned __stdcall recvMessage(SOCKET *p)
{
for (char buf[BUFLEN];;)
{
int cc = recv(*p, buf, BUFLEN, 0); // BUFLEN为缓冲区buf的长度,返回值:接收的字符数(>0)、对方已关闭(=0) 或连接出错(<0)
if (finish)
return 0;
if (cc > 0)
{
buf[cc] = '\0'; // ensure null-termination
printf("%s", buf); // 显示所接收的字符串
}
else if (cc == SOCKET_ERROR)
printf("Recv Error:\n%d\n", GetLastError());
else if (cc == 0)
printf("Recv connect closed.\n");
}
}
int main()
{
char buf[BUFLEN];
WSADATA wsadata;
WSAStartup(WSVERS, &wsadata);
struct sockaddr_in sin; // an Internet endpoint address
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET; // 因特网地址簇
printf("Input the IP address:\n");
gets(buf);
sin.sin_addr.s_addr = inet_addr(buf); // 服务器IP地址(32位)
printf("Input the IP port:\n");
gets(buf);
sin.sin_port = htons(atoi(buf)); // 服务器端口号(16位)
SOCKET sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); // socket descriptor
int ret = connect(sock, &sin, sizeof(sin)); // 连接到服务器.无错时,返回0;否则,返回SOCKET_ERROR ,可以调用函数WSAGetLastError取得错误代码
HANDLE h = _beginthreadex(NULL, 0, &recvMessage, &sock, 0, NULL);
for (;;)
{
gets(buf);
if (!strcmp(buf, "exit"))
break;
int cc = send(sock, buf, strlen(buf), 0); //把缓冲区buf的数据发送出去,len为要发送的字节数,返回值:(>0) 实际发送的字节数(≤len), (=0) 对方正常关闭,(=SOCKET_ERROR) 出错,用函数WSAGetLastError取错误码。
if (cc == SOCKET_ERROR)
printf("Send Error:\n%d\n", GetLastError());
else if (cc == 0)
printf("Send connect closed.\n");
}
finish = 1;
CloseHandle(h);
closesocket(sock); // 关闭套接字
WSACleanup(); // 卸载winsock library
system("pause");
}
服务器端程序:
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
#include <stdio.h>
#include <time.h>
#include <process.h>
#include <winsock2.h>
#define BUFLEN 2000
#define WSVERS MAKEWORD(2, 0)
#pragma comment(lib, "ws2_32.lib") //使用winsock 2.2 library
SOCKET ssock[BUFLEN]; //master & *p sockets
struct sockaddr_in ssin[BUFLEN];
int sbadd[BUFLEN], ssock_size = 0;
unsigned __stdcall server(int *p)
{
for (char msg[BUFLEN] = "Enter!", pts[BUFLEN];;)
{
time_t now = time(NULL);
sprintf(pts,
"ip: %d.%d.%d.%d port: %u\n"
"time: %s"
"message: %s\n"
"\n>>",
ssin[*p].sin_addr.S_un.S_un_b.s_b1,
ssin[*p].sin_addr.S_un.S_un_b.s_b2,
ssin[*p].sin_addr.S_un.S_un_b.s_b3,
ssin[*p].sin_addr.S_un.S_un_b.s_b4,
ssin[*p].sin_port,
ctime(&now),
msg);
printf(pts);
for (int i = 0; i < ssock_size; ++i)
send(ssock[i], pts, strlen(pts), 0);
if (!strcmp(msg, "Leave!"))
return closesocket(ssock[*p]), 0; //关闭连接套接字
int cc = recv(ssock[*p], msg, BUFLEN, 0);
if (cc > 0)
msg[cc] = 0;
else
sprintf(msg, "Leave!");
}
}
int main()
{
WSADATA wsadata;
WSAStartup(WSVERS, &wsadata); //加载winsock library,WSVERS为请求版本,wsadata返回系统实际支持的最高版本
struct sockaddr_in msin; //an Internet endpoint addresss
memset(&msin, 0, sizeof(msin)); //从&sin开始的长度为sizeof(sin)的内存清0 , sin为一个地址结构
msin.sin_family = AF_INET; //因特网地址簇(INET-Internet)
msin.sin_addr.s_addr = INADDR_ANY; //监听所有(接口的)IP地址(32位),0.0.0.0
msin.sin_port = htons(atoi("50500")); //监听的端口号(16位) 。atoi--把ascii转化为int,htons—主机序到网络序
SOCKET msock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); //创建套接字。参数:因特网协议簇(family),字节流,TCP协议号。 返回:要监听套接字的描述符或INVALID_SOCKET
bind(msock, (struct sockaddr *)&msin, sizeof(msin)); //通过msin把要监听的IP地址和端口号绑定到套接字上
listen(msock, 5); //建立长度为5的连接请求队列,并开始监听是否有连接请求到来,来了则放入队列
printf("Server Start to listen.\n");
while (!_kbhit()) //检测是否有按键 (什么时候执行?)
{
int alen = sizeof(struct sockaddr); //from-address length
ssock[ssock_size] = accept(msock, (struct sockaddr *)&ssin[ssock_size], &alen); //accept:如果有新的连接请求,返回连接套接字,否则,被阻塞,ssin包含客户端IP地址和端口号
sbadd[ssock_size] = ssock_size;
_beginthreadex(NULL, 0, &server, &sbadd[ssock_size++], 0, NULL);
}
closesocket(msock);
WSACleanup();
system("pause");
}
运行效果如下:
测试一下客户端是否能够连上老师在校园网搭的服务器(172.18.187.9:50500): 运行截屏(客户端):
和同学互测一下看看,作为服务器运行截屏: 作为客户端运行截屏:
finish
来中转,子线程在finish
值变化的时候不再接受消息。Related posts