操作系统学习笔记(四)---socket实现简单的聊天程序

一、socket相关资料、流程

Socket?

网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。

建立网络通信连接至少要一对端口号(socket)。socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口;HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。

Socket的英文原义是“孔”或“插座”。作为BSD UNIX的进程通信机制,取后一种意思。通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,可以用来实现不同虚拟机或不同计算机之间的通信。在Internet上的主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务。Socket正如其英文原义那样,像一个多孔插座。一台主机犹如布满各种插座的房间,每个插座有一个编号,有的插座提供220伏交流电, 有的提供110伏交流电,有的则提供有线电视节目。 客户软件将插头插到不同编号的插座,就可以得到不同的服务。

 

TCP与UDP

TCP---传输控制协议,提供的是面向连接、可靠的字节流服务。当客户和服务器彼此交换数据前,必须先在双方之间建立一个TCP连接,之后才能传输数据。TCP提供超时重发,丢弃重复数据,检验数据,流量控制等功能,保证数据能从一端传到另一端。

UDP---用户数据报协议,是一个简单的面向数据报的运输层协议。UDP不提供可靠性,它只是把应用程序传给IP层的数据报发送出去,但是并不能保证它们能到达目的地。由于UDP在传输数据报前不用在客户和服务器之间建立一个连接,且没有超时重发等机制,故而传输速度很快

TCP和UDP都是在传输层上的。简单来说,UDP发送数据的时候是不管数据有没有真正达到目的地的,所以传输起来速度就比较快了。但是同时也容易造成数据丢失。而TCP我们知道有三次握手建立,四次握手释放,所以传输更准确,但是速度可能会相对慢一些。

为确保正确地接收数据,TCP要求在目标计算机成功收到数据时发回一个确认(即ACK)。如果在某个时限内未收到相应的ACK,将重新传送数据包。如果网络拥塞,这种重新传送将导致发送的数据包重复。但是,接收计算机可使用数据包的序号来确定它是否为重复数据包,并在必要时丢弃它。

 

socket函数创建套接字

#include <sys/types.h>         

#include <sys/socket.h>

int socket(int domain, int type, int protocol);

socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。

正如可以给fopen的传入不同参数值,以打开不同的文件。创建socket的时候,也可以指定不同的参数创建不同的socket描述符,socket函数的三个参数分别为

domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。

type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等。

protocol:故名思意,就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。

注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。

当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。

 

bind函数

bind函数将一个地址族中的特定地址赋给socket,例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

函数的三个参数分别为:

 

sockfd:即socket描述字,它是通过socket()函数创建的,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。

addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,如ipv4对应的是:

struct sockaddr_in {

    sa_family_t    sin_family; /* address family: AF_INET */

    in_port_t      sin_port;   /* port in network byte order */

    struct in_addr sin_addr;   /* internet address */

};

 

/* Internet address. */

struct in_addr {

    uint32_t       s_addr;     /* address in network byte order */

};

ipv6对应的是:

struct sockaddr_in6 {

    sa_family_t     sin6_family;   /* AF_INET6 */

    in_port_t       sin6_port;     /* port number */

    uint32_t        sin6_flowinfo; /* IPv6 flow information */

    struct in6_addr sin6_addr;     /* IPv6 address */

    uint32_t        sin6_scope_id; /* Scope ID (new in 2.4) */

};

 

struct in6_addr {

    unsigned char   s6_addr[16];   /* IPv6 address */

};

Unix域对应的是:

#define UNIX_PATH_MAX    108

 

struct sockaddr_un {

    sa_family_t sun_family;               /* AF_UNIX */

    char        sun_path[UNIX_PATH_MAX];  /* pathname */

};

addrlen:对应的是地址的长度。

通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。

 

主机字节序与网络字节序

主机字节序就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的Big-Endian和Little-Endian的定义如下:

a) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。

b) Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

 

网络字节序:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。

 

所以:在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian。

 

listen(),connect()函数

如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。

int listen(int sockfd, int backlog);

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。

connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。

 

accept()函数

TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就向TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

accept函数的第一个参数为服务器的socket描述字,第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数为协议地址的长度。如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。

注意:accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。

 

close()函数

在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。

#include <unistd.h>

int close(int fd);

close一个TCP socket的缺省行为时把该socket标记为已关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。

注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。

 

附:Linux头文件整理

sys/types.h:数据类型定义

sys/socket.h:提供socket函数及数据结构

netinet/in.h:定义数据结构sockaddr_in

arpa/inet.h:提供IP地址转换函数

netdb.h:提供设置及获取域名的函数

sys/ioctl.h:提供对I/O控制的函数

sys/poll.h:提供socket等待测试机制的函数

 

 

其他在网络程序中常见的头文件

unistd.h:提供通用的文件、目录、程序及进程操作的函数

errno.h:提供错误号errno的定义,用于错误处理

fcntl.h:提供对文件控制的函数

time.h:提供有关时间的函数

crypt.h:提供使用DES加密算法的加密函数

pwd.h:提供对/etc/passwd文件访问的函数

shadow.h:提供对/etc/shadow文件访问的函数

pthread.h:提供多线程操作的函数

signal.h:提供对信号操作的函数

sys/wait.h、sys/ipc.h、sys/shm.h:提供进程等待、进程间通讯(IPC)及共享内存的函数

 

附:I/O读写操作函数

read()            write()

recv()             send()

readv()           writev()

recvmsg()      sendmsg()

recvfrom()      sendto()

#include<unistd.h>

1.    ssize_t read(int fd, void *buf, size_t count); 

2.    ssize_t write(int fd, const void *buf, size_t count); 

推荐使用recvmsg()/sendmsg()函数,这两个函数是最通用的I/O函数,实际上可以把上面的其它函数都替换成这两个函数

read函数是负责从fd中读取内容.当读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题。

write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数。失败时返回-1,并设置errno变量。 在网络程序中,当我们向套接字文件描述符写时有俩种可能。

1)write的返回值大于0,表示写了部分或者是全部的数据。

2)返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接)。

 

二、用父子进程实现简单的网络聊天程序(C/C++  运行环境Ubuntu)

为什么需要子进程?

为了实现聊天的功能,客户端和服务器都需要一个进程来读取连接,另一个进程来处理键盘输入。

以服务器为例,如果只有一个进程,它只能接收客户端的输入信息,之后再反馈给客户端,如何能在接收信息的同时又向客户端输入信息呢?那么就需要一个子进程来write

流程图及说明

服务器端首先创建套接字(socket),然后定义一个地址结构并将套接字绑定到地址上(bind),接着进行监听(listen)直到监听到客户端,接着创建一个子进程用于接受键盘输入并发送信息给客户端,同时父进程接受子进程发送的数据流并打印。如果客户端关闭,父进程利用信号处理函数杀死子进程。最终关闭服务器和客户端的套接字。

客户端程序首先创建套接字,然后尝试与服务器端连接(不用绑定到地址结构),成功连接则创建一个子进程用于接收客户端发送的数据流并打印,父进程用于接收键盘输入并发送数据流给服务器端。输入CTRL+C则子进程中调用信号处理函数杀死父进程。

代码及运行效果(照着别人的改的仅供参考...建议用Java提供的socket的API来实现)

client.c

#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h> 
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <arpa/inet.h>   
#include <signal.h>

#define CLTIP "127.0.0.1"
#define SRVPORT 10005
void handler()
{
     exit(0);
}
int main()
{
        /*创建一个套接字*/
        int clientsock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        if(clientsock < 0)
    	{
        	printf("socket creation failed\n");
        	exit(-1);
    	}
   		printf("socket create successfully.\n");
        /*定义一个地址结构*/
        struct sockaddr_in clientAddr;
    	clientAddr.sin_family = AF_INET;
    	clientAddr.sin_port = htons((u_short)SRVPORT);
    	clientAddr.sin_addr.s_addr = inet_addr(CLTIP);
        /*进行连接*/
        if(connect(clientsock, (struct sockaddr*)&clientAddr, sizeof(struct sockaddr)) < 0)
    	{
        	printf("Connect error.IP[%s], port[%d]\n", CLTIP, clientAddr.sin_port);
        	exit(-1);
    	}
    	printf("Connect to IP[%s], port[%d]\n", CLTIP, clientAddr.sin_port);
    	
        pid_t pid ;
        pid = fork();
        if(pid == -1)
                exit(0);
        if(pid == 0) //子进程复制接收数据并显示出来
        {
                char recvbuf[1024]={0};
                while(1)
                {
                        memset(recvbuf,0,sizeof(recvbuf));
                        int ret = read(clientsock ,recvbuf,sizeof(recvbuf));
                        if(ret == -1)
                        {
                               exit(0);
                        }
                        if(ret == 0) //连接关闭
                        {
                                printf("the server has closed\n");
                                kill(getppid(),SIGUSR1);
                                break;
                        }
                        else
                        {
                                printf("Get message from server:");
                                fputs(recvbuf,stdout);
                        }
                }
        }
        else //父进程负责从键盘接收输入并发送
        {
                signal(SIGUSR1,handler);
                char sendbuf[1024]={0}  ;
                while(fgets(sendbuf,sizeof(sendbuf),stdin)!=NULL)
                {
                        write(clientsock,sendbuf,strlen(sendbuf));
                        memset(&sendbuf,0,sizeof(sendbuf));
                }
        }
        close(clientsock);
        return 0;
}

server.c

#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h> 
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <arpa/inet.h>   
#include <signal.h>

#define SRVIP "127.0.0.1"
#define SRVPORT 10005
/*信号处理函数*/
void handler(int sig)
{
        exit(0);
}
int main()
{
        /* 创建一个套接字*/
        int serversocket= socket(AF_INET ,SOCK_STREAM,IPPROTO_TCP);
        if(serversocket < 0)
                exit(0);
        /*定义一个地址结构并填充*/
        struct sockaddr_in serverAddr;
    	serverAddr.sin_family=AF_INET;
    	serverAddr.sin_port = htons((u_short)SRVPORT);
    	serverAddr.sin_addr.s_addr = inet_addr(SRVIP);
     
        /*将套接字绑定到地址上*/
        if(bind(serversocket, (struct sockaddr*)&serverAddr, sizeof(struct sockaddr))==-1)
    	{
          printf("Bind error.IP[%s], Port[%d]\n", SRVIP, serverAddr.sin_port);
          exit(0);
    	}
    	printf("Bind successful.IP[%s], Port[%d]\n", SRVIP, serverAddr.sin_port);
        /*监听套接字,成为被动套接字*/
        if(listen(serversocket,10)==-1)
    	{
          printf("Listen error!\n");
          exit(0);
    	}
    	printf("Listening on port[%d]\n", serverAddr.sin_port);
    	
        struct sockaddr_in peeraddr;
        socklen_t peerlen = sizeof(peeraddr);
        int conn ;
        conn = accept(serversocket,(struct sockaddr*)&peeraddr,&peerlen);
        if(conn <0)
            exit(0);
        else
            printf("new client touch.\n");
        pid_t pid ;
        pid = fork();//创建一个新进程
        if(pid == -1)
        {
                 exit(0);
        }
        if(pid == 0)//子进程
        {
                signal(SIGUSR1,handler);          //注册用户定义信号1  键盘输入CTRL+C执行handler函数 
                char sendbuf[1024] = {0};
                while(fgets(sendbuf,sizeof(sendbuf),stdin)!=NULL)
                {
                        write(conn,sendbuf,sizeof(sendbuf)); 
                        memset(&sendbuf,0,sizeof(sendbuf));
                }
                 exit(0);
        }
        else    //父进程 用来获取数据 
        {
                char recvbuf [1024]={0};
                while(1)
                {
                        memset(recvbuf,0,sizeof(recvbuf));
                        int ret = read(conn ,recvbuf,sizeof(recvbuf));
                        if(ret == -1)
                        {
                                exit(0);
                        }
                        if(ret == 0) //对方已关闭 
                        {
                                printf("对方关闭\n");
                                break;
                        }
                        fputs(recvbuf,stdout);
                }
                kill(pid,SIGUSR1);           //发送信号,执行子进程中的handler函数(杀死子进程) 
                 exit(0);
        }
        /*关闭套接字*/
        close(serversocket);
        close(conn);
        return 0;
}

运行效果

乱码是因为打印一些中文信息(懒得改了)

三、pthread实现简单的网络聊天程序(C/C++ 运行环境Ubuntu)

pthread创建进程/线程及锁的相关代码

pthread_t:线程ID

pthread_attr_t:线程属性

pthread_create(&tid,&attr,function,parameter):创建一个线程

pthread_exit(NULL):终止当前线程

pthread_cancel():中断另外一个线程的运行

pthread_join(tid,NULL):等待线程编号为tid的线程运行到结束(这里不加&)

pthread_attr_init(&attr):初始化线程的属性

pthread_attr_setdetachstate():设置脱离状态的属性(决定这个线程在终止时是否可以被结合)

pthread_attr_getdetachstate():获取脱离状态的属性

pthread_attr_destroy():删除线程的属性

pthread_kill():向线程发送一个信号

 

pthread_mutex_init() 初始化互斥锁

pthread_mutex_destroy() 删除互斥锁

pthread_mutex_lock():占有互斥锁(阻塞操作)

pthread_mutex_trylock():试图占有互斥锁(不阻塞操作)。即,当互斥锁空闲时,将占有该锁;否则,立即返回。

pthread_mutex_unlock(): 释放互斥锁

pthread_cond_init():初始化条件变量

pthread_cond_destroy():销毁条件变量

pthread_cond_signal(): 唤醒第一个调用pthread_cond_wait()而进入睡眠的线程

pthread_cond_wait(): 等待条件变量的特殊条件发生

Thread-local storage(或者以Pthreads术语,称作线程特有数据):

pthread_key_create(): 分配用于标识进程中线程特定数据的键

pthread_setspecific(): 为指定线程特定数据键设置线程特定绑定

pthread_getspecific(): 获取调用线程的键绑定,并将该绑定存储在 value 指向的位置中

pthread_key_delete(): 销毁现有线程特定数据键

pthread_attr_getschedparam();获取线程优先级

pthread_attr_setschedparam();设置线程优先级

 

思路及流程

                 

在上一回的父进程子进程的基础上改进,取消父子进程改为多线程,C/S连接流程不再说明。简单介绍下代码中用到的函数

server.c中的void *run为服务器每连接一个客户端新创建一个线程时该线程所要做的工作,首先要把该客户端在服务器中对应的socket套接字(int值)作为参数传递进来,然后循环调用read函数接收即可,如果客户端关闭,调用pthread_exit(NULL)杀死该线程。

 

server.c中的void *sendmessage和client.c中的该函数的功能类似,都是接收键盘输入的数据并发送出去,且都作为一个线程的运行函数(server.c中在accept之前创建该线程)。server.c中是向所有客户端发送,所以需要一个全局的数组来存储已创建的客户端的socket套接字,循环调用write函数即可;而client.c中就没有这么麻烦,只有其自己的套接字,一个while循环即可。

 

下面再简单介绍下用到的pthread.h中的函数及数据结构

pthread_t tid;    //线程标识符

pthread_attr_t;   //线程属性   

pthread_attr_init() //设置线程属性

pthread_create()   //线程创建,参数依次为线程标识,属性,函数名称,函数参数

                         其中线程属性可用NULL设置为默认,函数参数没有则用NULL

                         如果有多个参数用一个结构体打包(只能传递void *指针)

                         且注意要在函数中进行指针类型转换再获取指针内容

pthread_join(tid,NULL)  //等待线程标识为tid的线程结束

pthread_exit()               //线程结束,参数一般设为0

 

server.c

#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h> 
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <arpa/inet.h>   
#include <signal.h>
#include<pthread.h>
#define CLTIP "127.0.0.1"
#define SRVPORT 10005
void *sendmessage(void *arg)
{
	char sendbuf[1024]={0}  ;
	int clientsock = *((int *) arg);
    while(fgets(sendbuf,sizeof(sendbuf),stdin)!=NULL)
    {
        write(clientsock,sendbuf,strlen(sendbuf));
        memset(&sendbuf,0,sizeof(sendbuf));
    }
 	close(clientsock);
    exit(0);
}
//父线程负责接收数据,子线程接收键盘输入并发送数据给服务器 
int main()
{
        /*创建一个套接字*/
        int clientsock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        if(clientsock < 0)
    	{
        	printf("socket creation failed\n");
        	exit(-1);
    	}
   		printf("socket create successfully.\n");
        /*定义一个地址结构*/
        struct sockaddr_in clientAddr;
    	clientAddr.sin_family = AF_INET;
    	clientAddr.sin_port = htons((u_short)SRVPORT);
    	clientAddr.sin_addr.s_addr = inet_addr(CLTIP);
        /*进行连接*/
        if(connect(clientsock, (struct sockaddr*)&clientAddr, sizeof(struct sockaddr)) < 0)
    	{
        	printf("Connect error.IP[%s], port[%d]\n", CLTIP, clientAddr.sin_port);
        	exit(-1);
    	}
    	printf("Connect to IP[%s], port[%d]\n", CLTIP, clientAddr.sin_port);
        pthread_t tid;
        pthread_create(&tid,NULL,sendmessage,&clientsock);  //子线程接受键盘输入并发送数据 
        char recvbuf[1024]={0};
        int ret;
        while(1)   //父线程接收服务器发送的数据 
        {
           	memset(recvbuf,0,sizeof(recvbuf));
            ret = read(clientsock ,recvbuf,sizeof(recvbuf)); 
			if (ret == -1 || ret == 0)
				break;
			printf("Get message from server:");
            fputs(recvbuf,stdout);
        }
		pthread_join(tid,NULL);                        
        return 0;
}

server.c

#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h> 
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <arpa/inet.h>   
#include <signal.h>
#include<pthread.h>
#define SRVIP "127.0.0.1"
#define SRVPORT 10005

int client_fd[10] = {-1};
int num_of_client = 0;
int flag = 0;
void *run(void *arg)
{
	int clientsocket = *((int *) arg);  //取内容 先强制转换
	char recvbuf[1024] = {0};
	while(1)                          //对应客户端关闭则结束线程 
	{
		int ret = read(clientsocket,recvbuf,sizeof(recvbuf));
		if(ret == 0)   //对方关闭 
		{
			printf("The client %d has closed\n",clientsocket);
			flag = 1;
			break;
		}
		printf("Get messages from client %d :",clientsocket);
		fputs(recvbuf,stdout);
	}
	close(clientsocket);
	pthread_exit(NULL); 
}
void *sendmessage(void *arg)
{
	char sendbuf[1024] ;
    memset(sendbuf,0,sizeof(sendbuf));
	while(fgets(sendbuf,sizeof(sendbuf),stdin)!=NULL)
    {
        for(int i=0;i<10;i++)
		{
			if(client_fd[i] != 0)
			{
				write(client_fd[i],sendbuf,sizeof(sendbuf)); 
			}
		}   
		memset(&sendbuf,0,sizeof(sendbuf));
    }
    pthread_exit(NULL);
} 
int main()
{
        /* 创建一个套接字*/
        int serversocket= socket(AF_INET ,SOCK_STREAM,IPPROTO_TCP);
       
        if(serversocket < 0)
                exit(0);
        /*定义一个地址结构并填充*/
        struct sockaddr_in serverAddr;
    	serverAddr.sin_family=AF_INET;
    	serverAddr.sin_port = htons((u_short)SRVPORT);
    	serverAddr.sin_addr.s_addr = inet_addr(SRVIP);
     
        /*将套接字绑定到地址上*/
        if(bind(serversocket, (struct sockaddr*)&serverAddr, sizeof(struct sockaddr))==-1)
    	{
          printf("Bind error.IP[%s], Port[%d]\n", SRVIP, serverAddr.sin_port);
          exit(0);
    	}
    	printf("Bind successful.IP[%s], Port[%d]\n", SRVIP, serverAddr.sin_port);
        /*监听套接字,成为被动套接字*/
        if(listen(serversocket,10)==-1)
    	{
          printf("Listen error!\n");
          exit(0);
    	}
    	printf("Listening on port[%d]\n", serverAddr.sin_port);
    
        struct sockaddr_in clientaddr;                     //用于accept返回协议地址 
        socklen_t clientaddr_len = sizeof(clientaddr_len);
        int *conn_fd; 
        
        pthread_t send_message_tid;
		pthread_create(&send_message_tid,NULL,sendmessage,NULL); 
        while(1)  //收发信息都在创建的线程中 
        {
        	conn_fd = (int*) malloc(sizeof(int));
        	*conn_fd = accept(serversocket,NULL,NULL);  //返回新的套接字,注意服务结束后要关闭 
        	if(*conn_fd != -1)
        	{
        		printf("accept a client \n");
        		client_fd[num_of_client++] = *conn_fd;
			}
        	pthread_t tid;   
			pthread_create(&tid,NULL,run,conn_fd);
			//不等待...
		}
        /*关闭套接字*/
        close(serversocket);
        return 0;
}

运行效果

上面为最终运行效果,这里稍加解释:首先运行服务器,每监听到一个客户端则输出”accept a client”,以上accept了2个客户端。服务器可以向所有的客户端发送信息,如”hello everyone”

每个客户端发送的信息会发送给服务器(这里没有实现客户端群聊),上图中服务器会显示从哪个socket接收了数据。

 

收工......

猜你喜欢

转载自blog.csdn.net/qq_37205708/article/details/86547590