目录
一、socket相关资料、流程
附:Linux头文件整理
附:I/O读写操作函数
二、用父子进程实现简单的网络聊天程序(C/C++ 运行环境Ubuntu)
client.c
server.c
运行效果
三、pthread实现简单的网络聊天程序(C/C++ 运行环境Ubuntu)
pthread创建进程/线程及锁的相关代码
思路及流程
server.c
server.c
运行效果
PS:现在以前端的角度写了个简易聊天室,github地址:https://github.com/ChenMingK/chat-room
一、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;
in_port_t sin_port;
struct in_addr sin_addr;
};
struct in_addr {
uint32_t s_addr;
};
ipv6对应的是:
struct sockaddr_in6 {
sa_family_t sin6_family;
in_port_t sin6_port;
uint32_t sin6_flowinfo;
struct in6_addr sin6_addr;
uint32_t sin6_scope_id;
};
struct in6_addr {
unsigned char s6_addr[16];
};
Unix域对应的是:
#define UNIX_PATH_MAX 108
struct sockaddr_un {
sa_family_t sun_family;
char sun_path[UNIX_PATH_MAX];
};
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接收了数据。
收工......