【日常积累】实验室作业Socket实现多个客户端相互通信。

声明

这是本菜鸡的笔记总结,可能会有一些自己的主观。如有错误还请指正。

所用的函数

  1. socket()
    功 能:创建套接字
    原 型: int socket(int domain, int type, int protocol);
    参 数:
    domain:协议族,通常为 AF_INET,表示 TCP/IP 协议
    type: socket 类型,如: SOCK_STREAM(指 TCP)和
    SOCK_DGRAM(指 UDP)等等
    protocol:套接口所用的协议,一般为 0
    返回值:
    成功: socket 文件描述符
    失败: -1,并设置 errno
  2. bind()
    功 能:将套接字和指定的端口相连
    原 型: int bind(int sock_fd, struct sockaddr_in *my_addr, int addrlen);
    参 数:sock_fd: socket 文件描述符
    my_addr:设置服务器信息的 sockaddr_in 结构体指针
    addrlen: sockaddr_in 结构体的长度
    返回值:
    成功: 0
    失败: -1,并设置 errno
    说 明:
    struct sockaddr_in{
    short int sin_family; //网络通信网络层协议
    AF_INET
    unsigned short int sin_port; //端口
    struct in_addr sin_addr; //IP 地址
    unsigned char sin_zero[8]; //让 sockaddr 与
    sockaddr_in 两个数据结构保持大小相同而保留的空字节
    };
  3. connect()
    功 能:连接服务器请求(客户端使用)
    原 型: int connect(int sock_fd, struct sockaddr *serv_addr,int addrlen);
    参 数:
    sock_fd: socket 文件描述符
    serv_addr:包含远端主机 IP 地址和端口号的指针
    addrlen: sockaddr_in 结构体的长度
    返回值:
    成功: 0
    失败: -1,并设置 errno
  4. listen()
    功 能:创建一个套接口并监听申请的连接
    原 型: int listen(int sock_fd, int backlog);
    参 数:
    sock_fd: socket 文件描述符
    backlog:请求队列中允许的最大请求数
    返回值:
    成功: 0
    失败: -1,并设置 errno
  5. accecpt()
    功 能:接受客户端的服务请求
    原 型: int accept(int sock_fd, struct sockadd_in* addr, int addrlen);
    参 数:
    sock_fd:被监听的 socket 文件描述符
    addr:包含客户端 IP 地址和端口号的指针
    addrlen: sockaddr_in 结构体的长度返回值:
    成功:客户端套接字描述符
    失败: -1,并设置 errno
  6. write()
    功 能:写入数据到 fd 中
    原 型: ssize_t write(int fd,const void *buf,size_t nbytes);
    参 数:
    fd: socket 文件描述符
    buf:字符串数据地址
    nbytes:字符串数据大小
    返回值:
    实际写入的字节数,小于 0 为写入错误
    6、 read()
    功 能:从 fd 中读取数据
    原 型: ssize_t read(int fd,void *buf,size_t nbyte)
    参 数:
    fd: socket 文件描述符
    buf:字符串数据地址
    nbyte:字符串数据大小
    返回值:

0: 实际读取的大小
=0:读到末尾了
<0:读取错误
8、 close()
功 能:关闭套接字
原 型: int close(sock_fd);
参 数:
sock_fd:要关闭的 socket 文件描述符
返回值:
成功: 0
失败: -1

进程线程的创建和释放

2.2:线程的创建

pthread_create (thread, attr, start_routine, arg)
thread:进程号(自己定义的)
attr:NULL
start_routine:开启的函数名称(也就是其首地址)
arg :NULL

2.3:资源的释放(在这里我使用的是unjoinable模式)
	pthread_detach(pthread_self());
	这个函数可以使我们的线程在退出时立即释放资源。

pthread有两种状态joinable状态和unjoinable状态,如果线程是joinable状态,当线程函数自己返回退出时或pthread_exit时都不会释放线程所占用堆栈和线程描述符(总计8K多)。只有当你调用了pthread_join之后这些资源才会被释放。若是unjoinable状态的线程,这些资源在线程函数退出时或pthread_exit时自动会被释放。
2.unjoinable属性可以在pthread_create时指定,或在线程创建后在线程中pthread_detach自己(我们这就是这样创建), 如:pthread_detach(pthread_self()),将状态改为unjoinable状态,确保资源的释放。或者将线程置为 joinable,然后适时调用pthread_join.
3.其实简单的说就是在线程函数开始加上 pthread_detach(pthread_self()),线程状态改变,在函数尾部直接调用pthread_exit()线程就会自动退出。

pthread_join()即是子线程合入主线程,主线程阻塞等待子线程结束,然后回收子线程资源。

#头文件解读
3.1:网络编程用的头文件
#include <sys/socket.h>
3.2:线程用的头文件
#include <pthread.h>

特定结构体。

4.1:sockaddr_in为协议族,地址,端口等必要信息构成的结构体,对信息的使用方便很多。

代码

服务器端的
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <unistd.h>

int SERVER_PORT = 8091;

typedef unsigned char u8;
typedef char cmd_key_t;
typedef int mid_t;
#define MESSAGE_LENGTH_MAX  512


typedef struct
{
	cmd_key_t cmd_key; //
	mid_t own;
	mid_t peer;
	u8 message[MESSAGE_LENGTH_MAX];
} client_message_t;


int Gfd[1024];


int _id2peer_trans_func_(int prid)
{
	//int socket_handler = prid --> cms->peer;
	int socket_handler = Gfd[prid];
	return(socket_handler);
}


void *_t_recv(void *arg)
{
	int fd = *((int *)arg);
	pthread_detach(pthread_self());
	while(1)
	{
		unsigned char buffer[1024];
		int ret = recv(fd,buffer,sizeof(client_message_t),MSG_WAITALL);
		if(ret > 0)
		{
			client_message_t *cms = (client_message_t *)buffer;
			if(cms->cmd_key  == 'L')
			{
				printf("[S]new login[%d],his handler is %d\r\n",cms->own,fd);
				Gfd[cms->own] = fd;
				strcpy(cms->message,"[S]welcome\r\n");
				send(_id2peer_trans_func_(cms->own),cms,sizeof(client_message_t),MSG_WAITALL);
			}
			else
			{
				printf("   [S] #ID%d/FD%d# --> @ID%d / FD%d@\r\n",
					cms->own,
					_id2peer_trans_func_(cms->own),
					cms->peer,
					_id2peer_trans_func_(cms->peer));

				send(_id2peer_trans_func_(cms->peer),cms,sizeof(client_message_t),MSG_WAITALL);
			}
		}
		else
		{
			perror("[S] recv:");
			break;
		}
	}

	printf("close the peer.\r\n");
	close(fd);
	pthread_exit(NULL);
}

int main()
{
	//调用 socket 函数返回的文件描述符
	int serverSocket;
	//声明两个套接字 sockaddr_in 结构体变量,分别表示客户端和服务器
	struct sockaddr_in server_addr;
	int addr_len = sizeof(server_addr);
	int client;
	if ((serverSocket = socket(AF_INET, SOCK_STREAM, 0)) < 0)
	{
		perror("socket");
	}
	bzero(&server_addr, sizeof(server_addr));
	//初始化服务器端的套接字,并用 htons 和 htonl 将端口和地址转成网络字节序
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons(SERVER_PORT);
	server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

	if (bind(serverSocket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
	{
		perror("connect");
	}

	if (listen(serverSocket, 5) < 0)
	{
		perror("listen");
	}

	printf("[S]Listening on port: %d\n", SERVER_PORT);
	while(1)
	{
		struct sockaddr_in clientAddr;
		client = accept(serverSocket, (struct sockaddr *)&clientAddr, &addr_len);
		if (client < 0)
		{
			perror("accept");
			continue;
		}
		pthread_t trecvid, tsendid;
		pthread_create(&trecvid, NULL, _t_recv, &client);
	}
}

这个服务端的函数的重点我在这个里解释一下,

1: typedef struct client_message_t,顾名思义这个是客户端发来的消息结构体 内容是消息的类型(最后要我们实现的就是可以发消息和文件、视频)、消息的发送方和接受方,消息内容
2: id2peer_trans_func 这个是数组下标和套接字映射函数。我们登录后要输入一个“机号”——数组下标,服务器就会把生成的客户端套接字存进对应下标的数组内。这可以”弱化“套接字的影响,即使我们退出重进只要你输入的是原来的“机号”你的小朋友还是可以根据“机号”和你交往。
3: void *_t_recv(void *arg)这就是我们开的线程,里面根据客户端信息体中的信息类型将功能分为2部分当cmd_key == 'L’时就是登录其它就是根据目标“机号转发信息的过程。

因为我们在进行多个客户端的通信中要知道对方的套接字,但是我们正常情况下只知道对方的编号(套接字是随机的,所以如果你退出后你再重新上线后套接字就又变了,非常难搞。)

客户端代码
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <unistd.h>

int SERVER_PORT = 8091;

typedef unsigned char u8;
typedef char cmd_key_t;
typedef int mid_t;
#define MESSAGE_LENGTH_MAX  512




typedef struct
{
	cmd_key_t cmd_key;
	mid_t own;
	mid_t peer;
	u8 message[MESSAGE_LENGTH_MAX];
} client_message_t;



void *_t_recv(void *arg)
{
	int fd = *((int *)arg);
	while(1)
	{
		unsigned char buffer[1024];
		int ret = recv(fd,buffer,sizeof(client_message_t),MSG_WAITALL);
		if(ret > 0)
		{
			client_message_t *cms = (client_message_t *)buffer;
			printf("   [C]message from[%d]:%s\r\n",cms->own,cms->message);
		}
	}
}

int main()
{
	//创建 socket 对象
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (0 > sockfd)
	{
		perror("socket");
		return -1;
	} //准备通信地址
	struct sockaddr_in addr = {};
	    addr.sin_family = AF_INET;
	addr.sin_port = htons(8091); //端口号
	addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //本地 ip
	  //连接
	if (0 > connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)))
	{
	 	perror("connect");
		return -1;
	 }
  
	pthread_t trecvid;
	    pthread_create(&trecvid, NULL, _t_recv, &sockfd);

	client_message_t cms;
	memset(&cms,0,sizeof(cms));
	printf("[C]input your id(0~1024),then Enter:");fflush(stdout);
      	scanf("%d",&cms.own);
	cms.cmd_key = 'L';
	  int r = send(sockfd,&cms,sizeof(cms),MSG_WAITALL);
	printf("[C]we are login to svr<%d Bytes>.\r\n",r);



	while(1)
	{
		cms.cmd_key = 'M';
		printf("[C]input peer id(0~1024),then Enter:");fflush(stdout);
		scanf("%d",&cms.peer);
		printf("[C]input message send to [%d],then Enter:",cms.peer);fflush(stdout);
		scanf("%s",cms.message);
		send(sockfd,&cms,sizeof(cms),MSG_WAITALL);
	}

}

认真读完服务端的化客户端就是小菜半碟。注意的也就是fflush函数,因为我们输入数据后要回车,但是这个回车在不加fflush的情况下会被后面的scanf获取。这不是我们想要的结果。

总结

这个代码的逻辑还是比较简单的,但要自己写出来不是那么简单的。

  • 17
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值