声明
这是本菜鸡的笔记总结,可能会有一些自己的主观。如有错误还请指正。
所用的函数
- 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 - 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 两个数据结构保持大小相同而保留的空字节
}; - connect()
功 能:连接服务器请求(客户端使用)
原 型: int connect(int sock_fd, struct sockaddr *serv_addr,int addrlen);
参 数:
sock_fd: socket 文件描述符
serv_addr:包含远端主机 IP 地址和端口号的指针
addrlen: sockaddr_in 结构体的长度
返回值:
成功: 0
失败: -1,并设置 errno - listen()
功 能:创建一个套接口并监听申请的连接
原 型: int listen(int sock_fd, int backlog);
参 数:
sock_fd: socket 文件描述符
backlog:请求队列中允许的最大请求数
返回值:
成功: 0
失败: -1,并设置 errno - accecpt()
功 能:接受客户端的服务请求
原 型: int accept(int sock_fd, struct sockadd_in* addr, int addrlen);
参 数:
sock_fd:被监听的 socket 文件描述符
addr:包含客户端 IP 地址和端口号的指针
addrlen: sockaddr_in 结构体的长度返回值:
成功:客户端套接字描述符
失败: -1,并设置 errno - 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获取。这不是我们想要的结果。
总结
这个代码的逻辑还是比较简单的,但要自己写出来不是那么简单的。