1.0 一个简单的TCP服务器(只服务一个客户端)
先看代码如下:
server.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
static void Usage(const char* proc)
{
printf("Usage:%s[local_ip][local_port]\n",proc);
}
int StartUp(const char* ip,int port)
{
int sock = socket(AF_INET,SOCK_STREAM,0);//创建套接字
if(sock < 0){
perror("socket");
exit(2);
}
struct sockaddr_in local;//IPV4
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = inet_addr(ip);
if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0){//绑定套接字
perror("bind");
exit(3);
}
if(listen(sock,5) < 0){//监听套接字
perror("listen");
exit(4);
}
return sock;//将最终得到的监听套接字返回
}
int main(int argc,char* argv[])
{
if(argc != 3){
Usage(argv[0]);
return 1;
}
int listen_sock = StartUp(argv[1],atoi(argv[2]));//获得监听套接字
while(1)
{
struct sockaddr_in client;//用来获得发送者的信息(ip地址和端口号)
socklen_t len = sizeof(client);
//建立连接,得到新套接字进行操作
int new_sock = accept(listen_sock,(struct sockaddr*)&client,&len);
if(new_sock < 0){
perror("accept");//if fail,again listen
continue;
}
printf("get a connect,ip is %s,port is %u\n",\
inet_ntoa(client.sin_addr),ntohs(client.sin_port));
while(1)
{//先读再写
char buf[1024];
ssize_t s = read(new_sock,buf,sizeof(buf)-1);
if(s > 0){//读成功
buf[s] = '\0';
printf("client#:%s\n",buf);
write(new_sock,buf,strlen(buf));//server hui xian
}
else if(s == 0){//读到的字节数为0,表明对端结束已经关闭
printf("client exit\n");
break;
}
else{//读失败
perror("read");
break;
}
}
close(new_sock);
}
return 0;
}
client.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
static Usage(const char* proc)
{
printf("Usage:%s[server_ip][server_prot]\n",proc);
}
int main(int argc,char* argv[])
{
if(argc != 3){
Usage(argv[0]);
return 1;
}
int sock = socket(AF_INET,SOCK_STREAM,0);//创建套接字
if(sock < 0){
perror("socket");
return 2;
}
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
if(connect(sock,(struct sockaddr*)&server,sizeof(server)) < 0){//请求与服务器建立TCP连接
perror("connect");
return 3;
}
while(1)
{//先写在读
printf("client#");
fflush(stdout);
char buf[1024];
ssize_t s = read(0,buf,sizeof(buf)-1);//从标准输入(键盘)中读数据到buf
if(s > 0){
buf[s-1] = '\0';
write(sock,buf,strlen(buf));
ssize_t _s = read(sock,buf,sizeof(buf)-1);//实现服务器的回显
if(_s > 0){
buf[_s] = '\0';
printf("server echo#%s\n",buf);
}
}
else if(s == 0){
printf("read ending\n");
exit(4);
}
else{
perror("read");
break;
}
}
return 0;
}
····第一次连接的现象
client.c
server.c
·····client.c结束后的现象
client.c
server.c
····再次重新连接后的现象
client.c
server.c
但是有个问题,上面实现的简单服务器,每次只能连接一个客户端,当有多个客户端同时请求与服务器连接时,服务器只能处理一个的请求与一个客户端相连,剩下的连接请求先阻塞在连接队列,当服务器处理完某个客户端的连接,再从队列里拿一个连接进行处理。如下。
37161端口
37162端口
服务器处理完37161,才处理37162,因为服务器在处理37161的时候,37162给服务器发消息,服务器不作回应。
但是只能进行一对一的连接,与我们平常所见到的服务器功能不符,单进程的服务器毕竟来说可服务的对象就很小,所以现在实现一个多进程的服务器。那么接下来我们来实现一个一对多的服务器。
2.0 实现TCP多进程服务器
server.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<fcntl.h>
#include<arpa/inet.h>
#include<netinet/in.h>
static void Usage(const char* proc)
{
printf("Usage:%s[local_ip][local_port]\n",proc);
}
int StartUp(const char* ip,int port)
{
int sock = socket(AF_INET,SOCK_STREAM,0);//create socket
if(sock < 0){
perror("socket");
exit(2);
}
struct sockaddr_in local;//IPV4
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = inet_addr(ip);
if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0){//bind
perror("bind");
exit(3);
}
if(listen(sock,10) < 0){//listen
perror("listen");
exit(4);
}
return sock;//return an "listen_sock";
}
int main(int argc,char* argv[])
{
if(argc != 3){
Usage(argv[0]);
return 1;
}
int listen_sock = StartUp(argv[1],atoi(argv[2]));//get listen_sock
while(1)
{
struct sockaddr_in client;//get sender's information
socklen_t len = sizeof(client);
//get real socket that we will operator
int new_sock = accept(listen_sock,(struct sockaddr*)&client,&len);
if(new_sock < 0){
perror("accept");//if fail,again listen
continue;
}
printf("get a connect,ip is %s,port is %u\n",\
inet_ntoa(client.sin_addr),ntohs(client.sin_port));
pid_t id = fork();
if(id < 0){
perror("fork");
close(new_sock);
}
else if(id == 0)
{//child
close(listen_sock);
if(fork() > 0){//again fork , create sun zi process and child process end
exit(0);
}
//sun zi process working
while(1)
{//first read,then write
char buf[1024];
ssize_t s = read(new_sock,buf,sizeof(buf)-1);
if(s > 0){//read successful
buf[s] = '\0';
printf("client#:%s\n",buf);
write(new_sock,buf,strlen(buf));//server hui xian
}
else if(s == 0){//read to file's end
printf("client exit\n");
break;
}
else{//read fail
perror("read");
break;
}
}
close(new_sock);
exit(0);
}
else
{//father
close(new_sock);
waitpid(id,NULL,0);
}
}
return 0;
}
client.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<fcntl.h>
#include<netinet/in.h>
#include<arpa/inet.h>
static Usage(const char* proc)
{
printf("Usage:%s[server_ip][server_prot]\n",proc);
}
int main(int argc,char* argv[])
{
if(argc != 3){
Usage(argv[0]);
return 1;
}
int sock = socket(AF_INET,SOCK_STREAM,0);//create socket
if(sock < 0){
perror("socket");
return 2;
}
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
if(connect(sock,(struct sockaddr*)&server,sizeof(server)) < 0){//connect
perror("connect");
return 3;
}
while(1)
{//first write,then read
printf("client#");
fflush(stdout);
char buf[1024];
ssize_t s = read(0,buf,sizeof(buf)-1);//read data from stdin
if(s > 0){
buf[s-1] = '\0';
write(sock,buf,strlen(buf));
ssize_t _s = read(sock,buf,sizeof(buf)-1);//shi xian server hui xian
if(_s > 0){
buf[_s] = '\0';
printf("server echo#%s\n",buf);
}
}
else if(s == 0){
printf("read ending\n");
exit(4);
}
else{
perror("read");
break;
}
}
close(sock);
return 0;
}
在server.c中,我们先fock了两个进程父进程和子进程。父进程的工作是不断地返回accept处拿new_sock,子进程的工作是不断地去处理new_sock。也就是说父进程只管拿,子进程只管处理。所以父进程关闭不需要的套接字new_sock,而只留下listen_sock ; 子进程关闭不需要的listen_sock,而留下new_sock.
细心地你也许会发现,可是为什么还在子进程中再次fock创建一个孙子进程?这也正是这段代码的巧妙之处。
通过之前的学习我们知道,子进程退出后变成僵尸进程,直到父进程调用wait或者waitpid来对子进程资源进行回收。所以在第一次fork完成后,我们父进程会调用waitpid等待函数去等待子进程结束,但是它目前是阻塞式等待。为了解决阻塞问题,我们在子进程中又创建了一个孙子进程,并且让子进程退出,让孙子进程去完成子进程的工作。而对于孙子进程来说,子进程的退出使孙子进程变成孤儿进程,被1号进程Init收养,若孙子进程退出也是由Init进程回收它,与父进程中的waitpid无关。这样当子进程退出时,父进程的waitpid对子进程资源进行了回收,并且回收后因为子进程已经不存在了,waitpid也失去了它的作用,就不会阻塞了。这就是这段代码的巧妙之处。
结果如图:
client1
client2
server
3.0 实现TCP多线程服务器
基于多线程的服务器和多进程的差不多。代码如下。
server.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<fcntl.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<pthread.h>
static void Usage(const char* proc)
{
printf("Usage:%s[local_ip][local_port]\n",proc);
}
int StartUp(const char* ip,int port)
{
int sock = socket(AF_INET,SOCK_STREAM,0);//create socket
if(sock < 0){
perror("socket");
exit(2);
}
struct sockaddr_in local;//IPV4
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = inet_addr(ip);
if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0){//bind
perror("bind");
exit(3);
}
if(listen(sock,10) < 0){//listen
perror("listen");
exit(4);
}
return sock;//return an "listen_sock";
}
void* request(void* arg)
{
int new_sock = (int)arg;
printf("get a new client\n");
while(1)
{//first read,then write
char buf[1024];
ssize_t s = read(new_sock,buf,sizeof(buf)-1);
if(s > 0){//read successful
buf[s] = '\0';
printf("client#:%s\n",buf);
write(new_sock,buf,strlen(buf));//server hui xian
}
else if(s == 0){//read to file's end
printf("client exit\n");
break;
}
else{//read fail
perror("read");
break;
}
}
}
int main(int argc,char* argv[])
{
if(argc != 3){
Usage(argv[0]);
return 1;
}
int listen_sock = StartUp(argv[1],atoi(argv[2]));//get listen_sock
while(1)
{
struct sockaddr_in client;//get sender's information
socklen_t len = sizeof(client);
//get real socket that we will operator
int new_sock = accept(listen_sock,(struct sockaddr*)&client,&len);
if(new_sock < 0){
perror("accept");//if fail,again listen
continue;
}
pthread_t id;
pthread_create(&id,NULL,&request,(void*)new_sock);
pthread_detach(id);
}
return 0;
}
client.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<fcntl.h>
#include<netinet/in.h>
#include<arpa/inet.h>
static Usage(const char* proc)
{
printf("Usage:%s[server_ip][server_prot]\n",proc);
}
int main(int argc,char* argv[])
{
if(argc != 3){
Usage(argv[0]);
return 1;
}
int sock = socket(AF_INET,SOCK_STREAM,0);//create socket
if(sock < 0){
perror("socket");
return 2;
}
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
if(connect(sock,(struct sockaddr*)&server,sizeof(server)) < 0){//connect
perror("connect");
return 3;
}
while(1)
{//first write,then read
printf("client#");
fflush(stdout);
char buf[1024];
ssize_t s = read(0,buf,sizeof(buf)-1);//read data from stdin
if(s > 0){
buf[s-1] = '\0';
write(sock,buf,strlen(buf));
ssize_t _s = read(sock,buf,sizeof(buf)-1);//shi xian server hui xian
if(_s > 0){
buf[_s] = '\0';
printf("server echo#%s\n",buf);
}
}
else if(s == 0){
printf("read ending\n");
exit(4);
}
else{
perror("read");
break;
}
}
close(sock);
return 0;
}
thread1
thread2
server
4.0 实现UDP服务器
UDP服务器进行读写时,用的不是read和write,而是以下两个函数。
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buff, size_t nbytes, int flags, struct sockaddr *from, socklen_t *addrlen);
ssize_t sendto(int sockfd, const void *buff, size_t nbytes, int flags, const struct sockaddr *to, socklen_t addrlen);
//均返回:若成功则为读或写的字节数,出错为-1
前三个参数:sockfd, buff, nbytes等同于read和write的前三个参数:描述字,指向读入或者写出缓冲区的指针,读写字节数。
函数sendto的参数to是一个含有数据将发往的协议地址(例如IP地址和端口号)的套接口地址结构,它的大小由addrlen来指定。函数recvfrom用数据报发送者的协议地址装填由from所指的套接口地址结构,存储在此套接口地址结构中的字节数也以addrlen所指的整数返回给调用者。注意,sendto的最后一个参数是一个整数值,而recvfrom的最后一个参数值是一个指向整数值的指针。
recvfrom的最后两个参数类似于accept的最后两个参数:返回时套接口地址结构的内容告诉我们是谁发送了数据报(UDP情况下)或是谁发起了连接(TCP情况下)。sendto的最后两个参数类似于connect的最后两个参数:我们用数据报将发往(UDP情况下)或与之建立连接(TCP情况下)的协议地址来装填套接口地址结构。
写一个长度为0的数据报是可行的,这也意味着对于数据报协议,recvfrom返回0值也是可行的;它不表示对方已经关闭了连接,这与TCP套接口上的read返回0的情况不同。由于UDP服务器也是服务器,所以需要绑定,但因为UDP是无连接的,所以不需要监听和连接,这就没有诸如关闭UDP连接之类的事情。
PS:默认情况recvfrom函数没有接收到对方数据时候是阻塞的
实现代码如下:
server.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<fcntl.h>
#include<arpa/inet.h>
#include<netinet/in.h>
static void Usage(const char* proc)
{
printf("Usage:%s[local_ip][local_port]\n",proc);
}
int main(int argc,char* argv[])
{
if(argc != 3){
Usage(argv[0]);
return 1;
}
int sock = socket(AF_INET,SOCK_DGRAM,0);//create socket
if(sock < 0){
perror("socket");
exit(2);
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(atoi(argv[2]));
local.sin_addr.s_addr = inet_addr(argv[1]);
if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0){//bind
perror("bind");
exit(3);
}
printf("get a client\n");
while(1)
{//先读再写
printf("[%s:%d]\n",inet_ntoa(local.sin_addr),ntohs(local.sin_port));
struct sockaddr_in client;//获得发送方的信心(ip地址和端口号)
socklen_t len = sizeof(client);
char buf[1024];
int s = recvfrom(sock,buf,sizeof(buf)-1,0,(struct sockaddr*)&client,&len);//与accept相似
if(s < 0){
perror("recvfrom");
continue;
}
else if(s > 0){
buf[s] = '\0';
printf("[%s:%d]# %s\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port),buf);
sendto(sock,buf,strlen(buf),0,(struct sockaddr*)&client,sizeof(client));//sendto类似于connect,这一步是在进行服务器回显
}
else{
printf("client exit!\n");
continue;
}
}
return 0;
}
client.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<fcntl.h>
#include<netinet/in.h>
#include<arpa/inet.h>
static Usage(const char* proc)
{
printf("Usage:%s[server_ip][server_prot]\n",proc);
}
int main(int argc,char* argv[])
{
if(argc != 3){
Usage(argv[0]);
return 1;
}
int sock = socket(AF_INET,SOCK_DGRAM,0);//create socket
if(sock < 0){
perror("socket");
return 2;
}
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
while(1)
{//先写再读
printf("client#");
fflush(stdout);
char buf[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t s = read(0,buf,sizeof(buf)-1);
if(s > 0){
buf[s-1] = '\0';
sendto(sock,buf,strlen(buf),0,(struct sockaddr*)&server,sizeof(server));
ssize_t _s = recvfrom(sock,buf,sizeof(buf)-1,0,(struct sockaddr*)&peer,&len);//实现服务器回显
if(_s > 0){
buf[_s] = '\0';
printf("[%s:%d]:%s\n",inet_ntoa(peer.sin_addr),ntohs(peer.sin_port),buf);
}
}
else if(s == 0){
printf("read ending\n");
exit(4);
}
else{
perror("read");
break;
}
}
close(sock);
return 0;
}
client1
client2
server