网络编程tcp通讯

网络编程

 1.跟前面进程间通信的关系
          前面学习的进程间通信方式(管道,信号,共享内存,消息队列,信号量)都只能用于同一台主机内部的不同进程间
          网络编程(套接字编程)也是属于进程间通信的一种,网络编程既可以在同一台主机(本地通信)内部不同进程间通信,也能在不同主机间的进程通信
   2.知识点概念
         网络有关的概念
         传输层的两个协议tcp和udp
             tcp单向通信
             tcp双向通信
             tcp点播
             tcp广播
             udp单向通信
             udp双向通信
         多路复用

 网络有关的概念

  1.网络模型
         计算机科学家为了方便我们这些小白理解复杂的网络通信过程,特意把网络分成了不同的层次(方便我们去理解)
     网络通信协议
         人为制定的游戏规则,网络模型中每个层次都有自己的通信协议
     通信协议族
         ip地址分为两种IPV4地址和IPV6
         IPV4地址:最常用的ip地址,32位的地址  比如:  192.168.16.2(点分十进制ip)
         IPV6地址:全世界上网的人多了,ip不够用,扩展位数为128位
     端口号
         本质上是个无符号的短整型数字(0---65535之间),用来区分同一台主机内部不同的网络进程
         程序员可以自己指定端口号,1024以内的端口号最好不要使用(很多被操作系统占用)

     常用的网络模型
         第一种:OSI七层模型
                   应用层     作用:开发特定的应用程序需要用到其中的部分协议  http协议(超文本传输协议)  ftp协议(文件传输协议)  telnet(远程登录)
                   会话层     作用:建立会话,对数据加密,解密
                   表示层     作用:数据的封装,解析    
                   传输层     作用:解决数据在网络中传输的问题  tcp协议和udp协议
                   网络层(ip层)  作用:解决路由(数据采取何种路径发送出去最优)问题   ip协议
                   数据链路层   作用:开发网卡的驱动,需要深入了解这个层次的协议
                   物理层         网络接口,真实的网卡
         第二种:TCP/IP模型
                   把七层模型合并简化成四层
                   应用层(原来的应用层,会话层,表示层合并) 
                   传输层
                   网际层
                   网络接口层(数据链路层和物理层合并)

 

 

 

 tcp协议

 1.tcp协议的通信流程(原理)
   2.tcp协议相关的接口函数
        (1)创建套接字(买手机)
           #include <sys/types.h>         
           #include <sys/socket.h>
           int socket(int domain, int type, int protocol);
                 返回值:成功  返回套接字的文件描述符
                         失败  -1
                   参数:domain --》通信协议族
                                   AF_INET   --》表示ipv4地址协议族
                                   AF_INET6  --》表示ipv6地址协议族
                         type --》套接字的类型
                                   SOCK_STREAM --》tcp套接字(流式套接字,数据流套接字)
                                   SOCK_DGRAM  --》udp套接字(数据报套接字)
                         protocol --》扩展协议,默认设置0,不用理会   
         (2)绑定ip和端口号
            int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);  //通用性强,使用ipv4或者ipv6都可以
            假设:int bind(int sockfd, const struct sockaddr_in *addr,socklen_t addrlen);  //局限性只能绑定ipv4地址
            假设:int bind(int sockfd, const struct sockaddr_in6 *addr,socklen_t addrlen); //局限性只能绑定ipv6地址
                   参数: sockfd --》套接字文件描述符
                         addr --》
                                  struct sockaddr       通用地址结构体,兼容ipv4和ipv6
                                  struct sockaddr_in    ipv4地址结构体(保存ipv4地址和端口号的)
                                  {
                                       sin_family;  //保存地址协议族AF_INET 
                                       struct in_addr sin_addr;    //保存你要绑定的ip地址(一定是绑定本地主机的ip)
                                       sin_port;    //保存你要绑定的端口号
                                  }
                                  sin_addr成员又是一个结构体
                                  {
                                       in_addr_t s_addr;  //最终用来存放ipv4地址的那个变量
                                  } 
                                  struct sockaddr_in6   ipv6地址结构体(保存ipv6地址和端口号的)
                          addrlen --》地址结构体的大小 sizeof()    
            (3)ip地址和端口号的转换--》大小端转换(字节序转换)
                    大端序:数据的高字节存放在低地址,低字节存放在高地址
                    小端序:数据的高字节存放在高地址,低字节存放在低地址
                    ubuntu系统:采用的是小端序存放数据
                    主机字节序:指的就是小端序
                    计算机网络协议:采用的是大端序
                    网络字节序:指的就是大端序
               主机字节序---》网络字节序
                     #include <netinet/in.h>
                     #include <arpa/inet.h>
                     转换ip地址
                           in_addr_t inet_addr(const char *cp);
                                    返回值:成功 返回转换得到的网络字节序ip
                                      参数:cp --》你要转换的那个字符串ip地址
                     转换端口号  
                           uint16_t htons(uint16_t hostshort);
                                    返回值:成功 返回转换得到的网络字节序端口号
                                      参数:hostshort --》你要转换的端口号
               网络字节序---》主机字节序
                     转换ip地址
                           char *inet_ntoa(struct in_addr in);
                                    返回值:成功 返回字符串ip地址
                                      参数:ipv4地址结构体中用来存放大端序ip的那个变量
                     转换端口号
                           uint16_t ntohs(uint16_t netshort);
             (4)连接服务器
                   看到地址结构体指针,条件反射知道究竟存放谁的ip,谁的端口号
                   int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
                         参数: sockfd --》套接字文件描述符
                               addr --》存放服务器的ip和端口号
                               addrlen --》地址结构体的大小 sizeof()
             (5)收发信息
                   有两组函数可以使用
                         第一组:read/write
                         第二组:recv/send
                   ssize_t send(int sockfd, const void *buf, size_t len, int flags);  //发送信息
                          返回值:成功 发送的字节数
                                  失败 -1
                            参数:sockfd --》套接字文件描述符
                                  buf --》存放你要发送的内容
                                  len --》你打算发送多少字节的数据
                                  flags --》设置为0
                   ssize_t recv(int sockfd, void *buf, size_t len, int flags);        //接收信息
                          返回值:成功  接收到的字节数
                                  失败  -1
                                  断开  0
                            参数:sockfd --》套接字文件描述符
                                  buf --》存放你接收的内容
                                  len --》你打算接收多少字节的数据
                                  flags --》设置为0
                          注意:如果对方一直没有信息发送过来recv/read会一直阻塞
              (6)监听 --》待机
                     int listen(int sockfd, int backlog);
                            参数:sockfd --》套接字文件描述符
                                  backlog --》最多允许多少个客户端同时连接一个服务器,一般5--10随便写个数字
                            比如:listen(sock,5); 最多运行5个客户端同时连接,但是如果客户端是依次错开连接服务器的,可以超过5个
              (7)接受客户端的连接请求(重点)
                     int accept(int socket, struct sockaddr *address,socklen_t *address_len);
                           返回值:成功 返回值新的套接字文件描述符(用来区分不同的客户端,服务器要跟哪个客户端聊天,使用对应的套接字即可)
                                   失败 -1
                             参数:sockfd --》套接字文件描述符
                                   address --》不需要你初始化,自动存放目前连接你的那个客户端的ip和端口号
                                   address_len --》地址结构体大小,要求是指针
                 总结accept函数的特点
                        特点一:如果没有客户端连接服务器,服务器会一直阻塞在accept的位置
                                如果有客户端成功连接服务器,accept就不会阻塞,返回新的套接字用于通信
                        特点二:accept每连接成功一个客户端,都会产生不同的新套接字
 

 遇到的问题和现象

   问题一:
         客户端强行退出会导致服务器recv不阻塞了???
   原因:
         无论客户端/服务器,只要断开了,recv/read都不会阻塞,并且返回0表示对方断开了
   解决方法:
         条件判断,发现对方断开,你也退出程序
   问题二:
         首次运行程序没有任何问题,强行退出以后,再次运行程序出现:绑定ip和端口号失败! : Address already in use错误
   原因:  
         linux系统当你的程序退出的时候,绑定的端口号不会立马释放调用(需要等待大概半分钟才会释放),所以你急急忙忙再次运行,提示这种错误(端口号被占用了)
   解决方法:
          方法一:耐心等待半分钟以后再来运行即可
          方法二:更换新的端口号(通过主函数传参去更换比较方便),重新编译程序运行
          方法三:使用setsockopt这个函数设置取消端口绑定限制
                  int setsockopt(int socket, int level, int option_name, const void *option_value, socklen_t option_len);
                         参数:socket --》套接字文件描述符
                               level --》SOL_SOCKET
                               option_name --》SO_REUSEADDR   重复使用端口号

 

 

 tcp单向通讯

#include "myhead.h"
/*
	通过男女朋友聊天的例子,辅助理解tcp通信的流程和接口函数
	男朋友的代码--》男朋友(客户端)发送信息给女朋友(服务器端)
	老师为了上课演示现象方便--》客户端和服务器都在同一个主机上
	同一台主机--》端口号不能一样
	不同的主机--》端口号相同也行,不相同也行
	
*/

int main()
{
	char sbuf[100];
	int tcpsock;
	int ret;
	//定义ipv4地址结构体变量
	struct sockaddr_in bindaddr;
	bzero(&bindaddr,sizeof(bindaddr));
	bindaddr.sin_family=AF_INET;
	bindaddr.sin_addr.s_addr=inet_addr("192.168.1.50");  //男朋友的ip地址
	bindaddr.sin_port=htons(10000);  //男朋友的端口号
	
	//定义ipv4地址结构体变量存放女朋友(服务器)的ip和端口号
	struct sockaddr_in serveraddr;
	bzero(&serveraddr,sizeof(serveraddr));
	serveraddr.sin_family=AF_INET;
	serveraddr.sin_addr.s_addr=inet_addr("192.168.1.50");  //女朋友的ip地址
	serveraddr.sin_port=htons(20000);  //女朋友的端口号
	
	//创建套接字
	tcpsock=socket(AF_INET,SOCK_STREAM,0);
	if(tcpsock==-1)
	{
		perror("创建套接字失败!\n");
		return -1;
	}
	
	//绑定ip和端口号
	ret=bind(tcpsock,(struct sockaddr *)&bindaddr,sizeof(bindaddr));
	if(ret==-1)
	{
		perror("绑定ip和端口号失败!\n");
		return -1;
	}
	
	//连接服务器
	ret=connect(tcpsock,(struct sockaddr *)&serveraddr,sizeof(serveraddr));
	if(ret==-1)
	{
		perror("连接服务器失败!\n");
		return -1;
	}
	
	while(1)
	{
		bzero(sbuf,100);
		printf("请输入要发送给服务器(女朋友)的信息!\n");
		scanf("%s",sbuf);
		//发送给服务器
		send(tcpsock,sbuf,strlen(sbuf),0);
	}
	
	close(tcpsock);
	return 0;
}

 

#include "myhead.h"
/*
	通过男女朋友聊天的例子,辅助理解tcp通信的流程和接口函数
	女朋友的代码--》男朋友(客户端)发送信息给女朋友(服务器端)
	老师为了上课演示现象方便--》客户端和服务器都在同一个主机上
	同一台主机--》端口号不能一样
	不同的主机--》端口号相同也行,不相同也行
	
*/

int main()
{
	char sbuf[100];
	int tcpsock;
	int newsock;
	int ret;
	//定义ipv4地址结构体变量
	struct sockaddr_in bindaddr;
	bzero(&bindaddr,sizeof(bindaddr));
	bindaddr.sin_family=AF_INET;
	bindaddr.sin_addr.s_addr=inet_addr("192.168.1.50");  //女朋友的ip地址
	bindaddr.sin_port=htons(20000);  //女朋友的端口号
	
	//定义ipv4地址结构体变量存放连接成功的那个客户端ip和端口号
	struct sockaddr_in clientaddr;
	int addrsize=sizeof(clientaddr);
	
	//创建套接字
	tcpsock=socket(AF_INET,SOCK_STREAM,0);
	if(tcpsock==-1)
	{
		perror("创建套接字失败!\n");
		return -1;
	}
	
	//绑定ip和端口号
	ret=bind(tcpsock,(struct sockaddr *)&bindaddr,sizeof(bindaddr));
	if(ret==-1)
	{
		perror("绑定ip和端口号失败!\n");
		return -1;
	}
	
	//监听
	ret=listen(tcpsock,6);
	if(ret==-1)
	{
		perror("监听失败!\n");
		return -1;
	}
	printf("旧的套接字是:%d\n",tcpsock);
	//接收客户端的连接请求     
	newsock=accept(tcpsock,(struct sockaddr *)&clientaddr,&addrsize);
	if(newsock==-1)
	{
		perror("接收客户端的连接请求失败!\n");
		return -1;
	}
	printf("新的套接字是:%d\n",newsock);
	while(1)
	{
		bzero(sbuf,100);
		//接收客户端发送过来的信息
		recv(newsock,sbuf,100,0);
		//recv(tcpsock,sbuf,100,0);  //错误示范,用错了套接字
		printf("服务器收到的内容是:%s\n",sbuf);
	}
	
	close(tcpsock);
	return 0;
}

 tcp双向通讯

#include "myhead.h"
/*
	tcp双向通信
	
*/
int tcpsock;
void *recvmsgfromserver(void *arg)
{
	char rbuf[100];
	int ret;
	while(1)
	{
		bzero(rbuf,100);
		ret=recv(tcpsock,rbuf,100,0);
		if(ret==0)  //说明对方(服务器断开了)
			exit(0);
		printf("服务器回复的信息是:%s\n",rbuf);
	}
}
int main()
{
	char sbuf[100];
	int ret;
	pthread_t id;
	//定义ipv4地址结构体变量
	struct sockaddr_in bindaddr;
	bzero(&bindaddr,sizeof(bindaddr));
	bindaddr.sin_family=AF_INET;
	bindaddr.sin_addr.s_addr=inet_addr("192.168.1.50");  //男朋友的ip地址
	bindaddr.sin_port=htons(10000);  //男朋友的端口号
	
	//定义ipv4地址结构体变量存放女朋友(服务器)的ip和端口号
	struct sockaddr_in serveraddr;
	bzero(&serveraddr,sizeof(serveraddr));
	serveraddr.sin_family=AF_INET;
	serveraddr.sin_addr.s_addr=inet_addr("192.168.1.50");  //女朋友的ip地址
	serveraddr.sin_port=htons(20000);  //女朋友的端口号
	
	//创建套接字
	tcpsock=socket(AF_INET,SOCK_STREAM,0);
	if(tcpsock==-1)
	{
		perror("创建套接字失败!\n");
		return -1;
	}
	
	//绑定ip和端口号
	ret=bind(tcpsock,(struct sockaddr *)&bindaddr,sizeof(bindaddr));
	if(ret==-1)
	{
		perror("绑定ip和端口号失败!\n");
		return -1;
	}
	
	//连接服务器
	ret=connect(tcpsock,(struct sockaddr *)&serveraddr,sizeof(serveraddr));
	if(ret==-1)
	{
		perror("连接服务器失败!\n");
		return -1;
	}
	
	//创建一个子线程专门用来接收信息
	pthread_create(&id,NULL,recvmsgfromserver,NULL);
	//主线程专门用来发送信息
	while(1)
	{
		bzero(sbuf,100);
		printf("请输入要发送给服务器(女朋友)的信息!\n");
		scanf("%s",sbuf);
		//发送给服务器
		send(tcpsock,sbuf,strlen(sbuf),0);
	}
	
	close(tcpsock);
	return 0;
}
#include "myhead.h"
/*
	服务器的代码
	
*/
int newsock;
void *sendmsgtoclient(void *arg)
{
	char rbuf[100];
	while(1)
	{
		bzero(rbuf,100);
		printf("请输入要发送给客户端的信息!\n");
		scanf("%s",rbuf);
		send(newsock,rbuf,strlen(rbuf),0);
	}
}
int main()
{
	char sbuf[100];
	int tcpsock;
	pthread_t id;
	int ret;
	//定义ipv4地址结构体变量
	struct sockaddr_in bindaddr;
	bzero(&bindaddr,sizeof(bindaddr));
	bindaddr.sin_family=AF_INET;
	bindaddr.sin_addr.s_addr=inet_addr("192.168.1.50");  //女朋友的ip地址
	bindaddr.sin_port=htons(20000);  //女朋友的端口号
	
	//定义ipv4地址结构体变量存放连接成功的那个客户端ip和端口号
	struct sockaddr_in clientaddr;
	int addrsize=sizeof(clientaddr);
	
	//创建套接字
	tcpsock=socket(AF_INET,SOCK_STREAM,0);
	if(tcpsock==-1)
	{
		perror("创建套接字失败!\n");
		return -1;
	}
	
	//绑定ip和端口号
	ret=bind(tcpsock,(struct sockaddr *)&bindaddr,sizeof(bindaddr));
	if(ret==-1)
	{
		perror("绑定ip和端口号失败!\n");
		return -1;
	}
	
	//监听
	ret=listen(tcpsock,6);
	if(ret==-1)
	{
		perror("监听失败!\n");
		return -1;
	}
	
	//接收客户端的连接请求     
	newsock=accept(tcpsock,(struct sockaddr *)&clientaddr,&addrsize);
	if(newsock==-1)
	{
		perror("接收客户端的连接请求失败!\n");
		return -1;
	}
	
	//创建一个线程专门用来发送信息
	pthread_create(&id,NULL,sendmsgtoclient,NULL);
	
	//主线程专门用来接收信息
	while(1)
	{
		bzero(sbuf,100);
		//接收客户端发送过来的信息
		ret=recv(newsock,sbuf,100,0);
		if(ret==0)  //说明客户端断开了
			exit(0);
		printf("服务器收到的内容是:%s\n",sbuf);
	}
	
	close(tcpsock);
	return 0;
}

 使用setsockopt这个函数设置取消端口绑定限制

#include "myhead.h"
/*
	tcp双向通信
	
*/
int tcpsock;
void *recvmsgfromserver(void *arg)
{
	char rbuf[100];
	int ret;
	while(1)
	{
		bzero(rbuf,100);
		ret=recv(tcpsock,rbuf,100,0);
		if(ret==0)  //说明对方(服务器断开了)
			exit(0);
		printf("服务器回复的信息是:%s\n",rbuf);
	}
}
int main(int argc,char **argv)
{
	char sbuf[100];
	int ret;
	pthread_t id;
	//定义ipv4地址结构体变量
	struct sockaddr_in bindaddr;
	bzero(&bindaddr,sizeof(bindaddr));
	bindaddr.sin_family=AF_INET;
	bindaddr.sin_addr.s_addr=inet_addr("192.168.1.50");  //男朋友的ip地址
	bindaddr.sin_port=htons(10000);  //男朋友的端口号
	
	//定义ipv4地址结构体变量存放女朋友(服务器)的ip和端口号
	struct sockaddr_in serveraddr;
	bzero(&serveraddr,sizeof(serveraddr));
	serveraddr.sin_family=AF_INET;
	serveraddr.sin_addr.s_addr=inet_addr("192.168.1.50");  //女朋友的ip地址
	serveraddr.sin_port=htons(20000);  //女朋友的端口号
	
	//创建套接字
	tcpsock=socket(AF_INET,SOCK_STREAM,0);
	if(tcpsock==-1)
	{
		perror("创建套接字失败!\n");
		return -1;
	}
	
	//取消端口号绑定限制
	int on=1;
	setsockopt(tcpsock,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
	
	//绑定ip和端口号
	ret=bind(tcpsock,(struct sockaddr *)&bindaddr,sizeof(bindaddr));
	if(ret==-1)
	{
		perror("绑定ip和端口号失败!\n");
		return -1;
	}
	
	//连接服务器
	ret=connect(tcpsock,(struct sockaddr *)&serveraddr,sizeof(serveraddr));
	if(ret==-1)
	{
		perror("连接服务器失败!\n");
		return -1;
	}
	
	//创建一个子线程专门用来接收信息
	pthread_create(&id,NULL,recvmsgfromserver,NULL);
	//主线程专门用来发送信息
	while(1)
	{
		bzero(sbuf,100);
		printf("请输入要发送给服务器(女朋友)的信息!\n");
		scanf("%s",sbuf);
		//发送给服务器
		send(tcpsock,sbuf,strlen(sbuf),0);
	}
	
	close(tcpsock);
	return 0;
}

 tcp点播、广播代码实现

1.点播 p2p(point to point)
         含义:一个客户端跟另外一个客户端通过服务器转发实现通信

 

 

#include "myhead.h"
/*
	tcp双向通信
	
*/
int tcpsock;
void *recvmsgfromserver(void *arg)
{
	char rbuf[2048];
	int ret;
	while(1)
	{
		bzero(rbuf,2048);
		ret=recv(tcpsock,rbuf,2048,0);
		if(ret==0)  //说明对方(服务器断开了)
			exit(0);
		printf("服务器回复的信息是:%s\n",rbuf);
	}
}
int main()
{
	char sbuf[2048];
	char someip[20];
	char someport[6];
	char truemsg[100];
	char broadcastmsg[100];
	int ret;
	pthread_t id;
	//定义ipv4地址结构体变量
	struct sockaddr_in bindaddr;
	bzero(&bindaddr,sizeof(bindaddr));
	bindaddr.sin_family=AF_INET;
	bindaddr.sin_addr.s_addr=inet_addr("192.168.1.50");  //男朋友的ip地址
	bindaddr.sin_port=htons(10001);  //男朋友的端口号
	
	//定义ipv4地址结构体变量存放女朋友(服务器)的ip和端口号
	struct sockaddr_in serveraddr;
	bzero(&serveraddr,sizeof(serveraddr));
	serveraddr.sin_family=AF_INET;
	serveraddr.sin_addr.s_addr=inet_addr("192.168.1.50");  //女朋友的ip地址
	serveraddr.sin_port=htons(20000);  //女朋友的端口号
	
	//创建套接字
	tcpsock=socket(AF_INET,SOCK_STREAM,0);
	if(tcpsock==-1)
	{
		perror("创建套接字失败!\n");
		return -1;
	}
	
	//取消端口号绑定限制
	int on=1;
	setsockopt(tcpsock,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
	
	//绑定ip和端口号
	ret=bind(tcpsock,(struct sockaddr *)&bindaddr,sizeof(bindaddr));
	if(ret==-1)
	{
		perror("绑定ip和端口号失败!\n");
		return -1;
	}
	
	//连接服务器
	ret=connect(tcpsock,(struct sockaddr *)&serveraddr,sizeof(serveraddr));
	if(ret==-1)
	{
		perror("连接服务器失败!\n");
		return -1;
	}
	
	//创建一个子线程专门用来接收信息
	pthread_create(&id,NULL,recvmsgfromserver,NULL);
	//主线程专门用来发送信息
	while(1)
	{
		bzero(sbuf,2048);
		printf("请输入要发送给服务器的信息!\n");
		printf("三种选择: 1. getlist表示获取在线客户端信息 2. p2p点播  3. all广播!\n");
		scanf("%s",sbuf);
		//判断你输入的内容
		if(strcmp(sbuf,"getlist")==0)  //表示你这个客户端想让服务器发送在线客户端信息给你
		{
			//发送给服务器
			send(tcpsock,sbuf,strlen(sbuf),0);
		}
		else if(strcmp(sbuf,"p2p")==0) //表示我想要点播
		{
			bzero(someip,20);
			bzero(someport,6);
			bzero(sbuf,2048);
			//键盘输入要点播的目标客户端ip和端口号
			printf("请输入目标ip和端口号!\n");
			scanf("%s",someip);
			scanf("%s",someport);
			printf("请输入要发送的信息!\n");
			scanf("%s",truemsg);
			//拼接字符串,把信息按照你制定的通信规则发送给服务器
			sprintf(sbuf,"p2p@%s@%s@%s",someip,someport,truemsg);
			send(tcpsock,sbuf,strlen(sbuf),0);
		}
		else if(strcmp(sbuf,"all")==0) //表示我想要广播
		{
			bzero(sbuf,2048);
			bzero(broadcastmsg,100);
			printf("请输入要广播的信息!\n");
			scanf("%s",broadcastmsg);
			sprintf(sbuf,"all@%s",broadcastmsg);
			printf("%s\n",sbuf);
			send(tcpsock,sbuf,strlen(sbuf),0);
		}
			
		
	}
	
	close(tcpsock);
	return 0;
}
#include "myhead.h"
/*
	服务器的代码
	
*/

//定义单链表存放目前连接成的所有的客户端信息
struct clientlist
{
	//数据域
	int sockfd; //套接字
	char ipbuf[16]; //ip地址
	unsigned short portnum; //端口号
	//指针域
	struct clientlist *next;
};

struct clientlist *myhead;

//初始化链表头结点
struct clientlist *list_init()
{
	struct clientlist *head=malloc(sizeof(struct clientlist));
	head->next=NULL;
	return head;
}

//尾插结点
int insert_tail(struct clientlist *node,struct clientlist *head)
{
	struct clientlist *p=head;
	while(p->next!=NULL)
		p=p->next;
	p->next=node;
}

//专门接收客户端发送过来的信息
void *recvclientmsg(void *arg)
{
	struct clientlist *temp=(struct clientlist *)arg;
	char rbuf[2048];
	char allmsg[2048];
	int ret;
	while(1)
	{
		bzero(rbuf,2048);
		/*
			根据我们制定的通信规则,客户端发过来的信息无非就三种
			第一种:getlist                         想要所有的客户端信息
			第二种:p2p@某个ip@某个端口号@真实信息  想要点播
			第三种:all@真实的信息                  想要广播给所有的人
			//第四种:emoji@表情包的编号
		*/
		ret=recv(temp->sockfd,rbuf,2048,0);
		if(ret==0) //某个客户端断开连接
		{
			printf("%s:%hu这个客户端断开了!\n",temp->ipbuf,temp->portnum);
			//从单链表中删除断开连接的这个客户端(两个指针一前一后配合删除)
			struct clientlist *delp1=myhead;
			struct clientlist *delp2=myhead->next;
			while(delp2!=NULL)
			{
				if(delp2->sockfd==temp->sockfd)  //说明找到了
				{
					close(temp->sockfd);
					delp1->next=delp2->next;
					delp2->next=NULL;
					free(delp2);
					//结束当前线程
					pthread_exit(NULL);
				}
				delp1=delp1->next;
				delp2=delp2->next;
			}
			
		}
		//打印收到的某个客户端的信息
		printf("%s:%hu这个客户端发送了信息是:%s\n",temp->ipbuf,temp->portnum,rbuf);
		//判断收到的信息
		if(strcmp(rbuf,"getlist")==0) //说明这个客户端想让我(服务器)给它发送所有客户端信息
		{
			//遍历当前存放客户端信息的链表,把客户端的ip和端口号拼接一起发送给对应的客户端
			bzero(allmsg,2048);
			struct clientlist *p=myhead;
			while(p->next!=NULL)
			{
				p=p->next;
				sprintf(allmsg+strlen(allmsg),"%s@%hu\n",p->ipbuf,p->portnum);
			}
			//把刚才拼接好的信息发送给对应的客户端
			send(temp->sockfd,allmsg,strlen(allmsg),0);
		}
		else //收到的是其他信息(点播/广播的信息)
		{
			//拆分字符串
			char *p=strtok(rbuf,"@");
			//判断收到的信息第一个字段 
			if(strcmp(p,"p2p")==0)  //点播
			{
				//进一步切割,得到对方的ip,端口号,真实信息
				char *p1=strtok(NULL,"@");
				char *p2=strtok(NULL,"@");
				char *p3=strtok(NULL,"@");
				//遍历链表找到这个客户端,把p3(真实的信息发送给对方)
				struct clientlist *temp1=myhead;
				int portn=atoi(p2); //端口号字符串转换成整数
				while(temp1->next!=NULL)
				{
					temp1=temp1->next;
					if(strcmp(temp1->ipbuf,p1)==0 && temp1->portnum==portn)
					{
						bzero(allmsg,2048);
						//把发送信息过来的那个客户端的ip和端口号跟p3一起拼接,然后发送给目标客户端
						sprintf(allmsg,"从%s:%hu的主机发送过来:%s",temp->ipbuf,temp->portnum,p3);
						send(temp1->sockfd,allmsg,strlen(allmsg),0);
					}
				}
			}
			else if(strcmp(p,"all")==0)  //广播
			{
				//进一步切割,得到需要广播真实信息
				char *p4=strtok(NULL,"@");
				//遍历链表把信息发送给所有的人(排除自己)
				struct clientlist *temp2=myhead;
				while(temp2->next!=NULL)
				{
					temp2=temp2->next;
					if(strcmp(temp2->ipbuf,temp->ipbuf)==0 && temp2->portnum==temp->portnum)
						continue; //排除自己
					send(temp2->sockfd,p4,strlen(p4),0);
				}
			}
		}
	}
}
int main()
{
	char sbuf[100];
	int tcpsock;
	int newsock;
	pthread_t id;
	int ret;
	//定义ipv4地址结构体变量
	struct sockaddr_in bindaddr;
	bzero(&bindaddr,sizeof(bindaddr));
	bindaddr.sin_family=AF_INET;
	bindaddr.sin_addr.s_addr=inet_addr("192.168.1.50");  //女朋友的ip地址
	bindaddr.sin_port=htons(20000);  //女朋友的端口号
	
	//定义ipv4地址结构体变量存放连接成功的那个客户端ip和端口号
	struct sockaddr_in clientaddr;
	int addrsize=sizeof(clientaddr);
	
	//初始化链表头结点
	myhead=list_init();
	
	//定义属性变量并初始化
	pthread_attr_t myattr;
	pthread_attr_init(&myattr);
	
	//设置分离属性
	pthread_attr_setdetachstate(&myattr,PTHREAD_CREATE_DETACHED);
	
	//创建套接字
	tcpsock=socket(AF_INET,SOCK_STREAM,0);
	if(tcpsock==-1)
	{
		perror("创建套接字失败!\n");
		return -1;
	}
	
	//取消端口号绑定限制
	int on=1;
	setsockopt(tcpsock,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
	
	//绑定ip和端口号
	ret=bind(tcpsock,(struct sockaddr *)&bindaddr,sizeof(bindaddr));
	if(ret==-1)
	{
		perror("绑定ip和端口号失败!\n");
		return -1;
	}
	
	//监听
	ret=listen(tcpsock,6);
	if(ret==-1)
	{
		perror("监听失败!\n");
		return -1;
	}
	
	//不断地接收不同客户端的连接请求
	while(1)
	{
		//接收客户端的连接请求     
		newsock=accept(tcpsock,(struct sockaddr *)&clientaddr,&addrsize);
		if(newsock==-1)
		{
			perror("接收客户端的连接请求失败!\n");
			return -1;
		}
		printf("新的套接字是:%d\n",newsock);
		printf("新连接成功的那个客户端ip是:%s\n",inet_ntoa(clientaddr.sin_addr));
		printf("新连接成功的那个客户端端口号是:%hu\n",ntohs(clientaddr.sin_port));
		//把连接成功的客户端信息存放到链表
		struct clientlist *newnode=malloc(sizeof(struct clientlist));
		newnode->sockfd=newsock;
		strcpy(newnode->ipbuf,inet_ntoa(clientaddr.sin_addr));
		newnode->portnum=ntohs(clientaddr.sin_port);
		newnode->next=NULL;
		insert_tail(newnode,myhead);
		
		//立马创建一个线程--》专门接收这个客户端发送过来的信息
		//每连接成功一个客户端,服务器都有一个专门的线程接收信息,这样子可以并发接收不同客户端同时发送的信息
		pthread_create(&id,&myattr,recvclientmsg,newnode);
	}
	
	close(tcpsock);
	return 0;
}

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hqb_newfarmer

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值