socket是实现进程通信的一种重要方式,本文将通过socket编程实现服务器进程与客户端进程之间的通信,并在通信之外实现猜数字的小游戏。
1. 设计思路
本文设计的C/S结构的猜数字游戏功能如下:服务器端自动生成一个1-100之间的随机数字,用户在客户端根据提示输入所猜的数字,若用户猜的数字和服务器所生成的数字相同,则游戏通关,用户可选择是否进入下一轮。当用户输入负数时,退出游戏。
设计要点如下:
-
服务端通信部分: 为实现客户端与服务器进程的相互通信,在服务端,首先创建一个socket,设置socket属性,并调用函数
bind()
绑定IP地址、端口等信息;然后调用函数listen()
开启监听,调用函数accept()
阻塞,等待客户端的连接请求;接下来,调用函数recv()
和send()
收发数据;最后关闭网络连接和监听。 -
客户端通信部分: 在客户端,也是先调用函数
socket()
创建一个socket,设置socket属性;然后调用函数connect()
连接服务器;调用函数recv()
和send()
收发数据,最后关闭网络连接。服务端与客户端的通信过程如下图所示。
-
服务端处理方式: 服务端采用多线程的方式处理客户端的请求。主线程每收到一个来自客户端的TCP连接请求,就调用
pthread_create()
函数创建一个子线程与之对应连接。一个线程处理一个客户端,从而实现了一个服务器与多个客户端之间的通信。 -
猜数字功能设计: server生成一个1到100之间的随机数,client去猜。client把每次猜的数字发给server。server收到后,会把猜的数字偏大、偏小、或是猜对的提示发给client。如果client不想继续猜了,发给server一个负数,则退出游戏。否则,一直到client猜对数字,用户选择不再进行下一轮,游戏结束。
2. 程序代码
2.1 服务器端server.c
服务器端代码server.c的具体实现如下:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <time.h>
#define PORT 1500//端口号
#define BACKLOG 5/*最大监听数*/
#define MAX_DATA 1024//接收到的数据最大长度
/*
*判断client猜的数字answer和server想的数字num是否相等
*若相等,返回1;不相等,则返回0
*/
int isEqual(int c, int answer, int num) {
if (answer < num) {
send(c, "Too small! Please guess again:", MAX_DATA, 0);
printf("Thinking: %d is too small!\n", answer);
return 0;
}
else if (answer > num) {
send(c, "Too big! Please guess again:", MAX_DATA, 0);
printf("Thinking: %d is too big!\n", answer);
return 0;
}
else if (answer == num) {
send(c, "Bingo!", MAX_DATA, 0);
printf("Thinking: Bingo, %d is the right answer!\n", answer);
return 1;
}
}
void guess_num(int *arg) {
int c = *arg;
char myrecv[MAX_DATA]; //储存收到的信息
char mysend[MAX_DATA]; //储存发出的信息
srand((int)time(NULL));
int num = rand() % 100 + 1; //server想的数字为[1,100)的随机数
send(c, "I'm thinking a number between 1 and 100! Guess what I think?", MAX_DATA, 0);
int bingo = 0; //bingo标志是否猜对,若猜对,bingo=0;否则,bingo=1
while (bingo == 0) {
memset(myrecv, '\0', sizeof(myrecv));
memset(mysend, '\0', sizeof(mysend));
recv(c, myrecv, MAX_DATA, 0);
printf("The client guesses %s\n", myrecv);
int answer = atoi(myrecv);
if (answer < 0) { //若client输入了一个负数,则退出游戏
send(c, "Exit game...", MAX_DATA, 0);
printf("Sent: Exit game...\n");
break;
}
bingo = isEqual(c, answer, num);
}
pthread_exit(NULL);
}
int main() {
int sockfd, new_fd;/*socket句柄和建立连接后的句柄*/
struct sockaddr_in my_addr;/*本方地址信息结构体,下面有具体的属性赋值*/
struct sockaddr_in their_addr;/*对方地址信息*/
int sin_size;
sockfd = socket(AF_INET,SOCK_STREAM,0);//建立socket
if (sockfd == -1) {
printf("socket failed:%d", errno);
return -1;
}
my_addr.sin_family = AF_INET;/*该属性表示接收本机或其他机器传输*/
my_addr.sin_port = htons(PORT);/*端口号*/
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);/*IP,括号内容表示本机IP*/
bzero(&(my_addr.sin_zero), 8);/*将其他属性置0*/
if (bind(sockfd, (struct sockaddr*)&my_addr, sizeof(struct sockaddr)) < 0) {
//绑定地址结构体和socket
printf("bind error");
return -1;
}
printf("bind OK\n");
listen(sockfd, BACKLOG);//开启监听 ,第二个参数是最大监听数
printf("listening\n");
while (1) {
sin_size = sizeof(struct sockaddr_in);
new_fd = accept(sockfd, (struct sockaddr*)&their_addr, &sin_size);//在这里阻塞知道接收到消息,参数分别是socket句柄,接收到的地址信息以及大小
if (new_fd == -1) {
continue;
}
else {
printf("accept: c = %d, ip = %s, port = %d\n", new_fd, inet_ntoa(my_addr.sin_addr), ntohs(my_addr.sin_port));
send(new_fd, "Connect sucess\n", MAX_DATA, 0);//发送内容,参数分别是连接句柄,内容,大小,其他信息(设为0即可)
pthread_t id; //新建线程,可连接多个客户端
pthread_create(&id, NULL, (void *)guess_num, &new_fd);
}
}
close(sockfd);
return 0;
}
2.2 客户端client.c
客户端代码client.c的具体实现如下:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <pthread.h>
#define DEST_PORT 1500//目标地址端口号
#define DEST_IP "127.0.0.1"/*目标地址IP,这里设为本机*/
#define MAX_DATA 1024//接收到的数据最大长度
int main() {
int sockfd, new_fd;/*cocket句柄和接受到连接后的句柄 */
struct sockaddr_in dest_addr;/*目标地址信息*/
char myrecv[MAX_DATA];//储存接收数据
char mysend[MAX_DATA];//储存发送数据
sockfd = socket(AF_INET,SOCK_STREAM,0);/*建立socket*/
if (sockfd==-1) {
printf("socket failed:%d",errno);
}
//参数意义见上面服务器端
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(DEST_PORT);
dest_addr.sin_addr.s_addr = inet_addr(DEST_IP);
bzero(&(dest_addr.sin_zero), 8);
if (connect(sockfd, (struct sockaddr*)&dest_addr, sizeof(struct sockaddr)) == -1) {
//连接方法,传入句柄,目标地址和大小
printf("connect failed:%d",errno);//失败时可以打印errno
}
else {
memset(myrecv, '\0', sizeof(myrecv));
recv(sockfd,myrecv,MAX_DATA,0); //接收"Connect sucess"
printf("Received: %s\n", myrecv);
recv(sockfd,myrecv,MAX_DATA,0); //接收"I'm thinking a number ..."
printf("Received: %s\n", myrecv);
while (1) {
memset(mysend, '\0', sizeof(mysend));
scanf("%s", mysend);
send(sockfd, mysend, MAX_DATA, 0);
memset(myrecv, '\0', sizeof(myrecv));
recv(sockfd, myrecv, MAX_DATA, 0);
if(strcmp(myrecv, "Bingo!") == 0 ||strcmp(myrecv, "Exit game...") == 0) {
printf("%s\n", myrecv);
break;
}
printf("Received: %s\n", myrecv);
}
}
close(sockfd);//关闭socket
return 0;
}
3. 运行结果
-
若可执行文件未生成,首先使用
gcc server.c -lpthread -o server
与gcc client.c -lpthread -o client
命令生成可执行文件server
与client
;若可执行文件已存在,转步骤2。(注意: 由于pthread
库不是Linux系统默认的库,连接时需要使用库libpthread.a
,所以在使用pthread_create
创建线程时,在编译中要加-lpthread
参数。) -
使用
./server
命令运行服务端程序,如下图所示。
-
使用
./client
命令运行客户端程序。启动3个客户端,均显示已和服务器连接成功,如下图所示。
-
用户根据提示(“Too small”/“Too big”)在客户端重复输入所猜数字,直至猜对,屏幕输出“Bingo”。若用户一直未猜对,输入任意负数即可退出游戏,客户端程序结束。如下图所示。