> 文章列表 > Linux网络套接字(二)

Linux网络套接字(二)

Linux网络套接字(二)

学习任务:

继网络套接字(一),继续学习套接字socket编程接口(已经学习了socket和bind),实现TCP客户端/服务器(单连接版本, 多进程版本, 多线程版本),并且理解tcp服务器建立连接, 发送数据, 断开连接的流程。

1.socket编程接口

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,socklen_t address_len);// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

接口解析

// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);

函数功能:由于服务器需要周而复始地等待客户端和自己建立连接,因此该函数由服务器使用,功能是等待用户连接,进行系统侦听请求。

第一个参数sockefd:由socket接口创建的套接字fd,不过需要注意

第二个参数backlog:套接字排队的最大连接个数(建议5~10),即申请连接的客户端的个数。

返回值:成功返回0,错误返回-1。

// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);

函数功能:接收用户连接请求,并返回一个新的套接字描述符用于与客户端通信。

  

第一个参数sockfd:由socket接口创建的套接字fd。

第二个参数addr:用于保存客户端的进程协议地址的结构体。

第三个参数addrlen:addr的大小。

返回值:返回一个新的套接字描述符。

// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

函数功能:建立连接。

第一个参数sockfd:由accept接口创建的套接字描述符。

第二个参数addr:套接字地址结构的指针。

第三个参数addrlen:addr的大小。

  

返回值:成功返回0。

单进程版本TCP(客户端/服务器)

单进程版本没有人会去使用,因为这种版本只能是一对一的连接,很明显不能符合业务要求的,就好比我们打开一个学习软件去学习,同学A先打开了,那么同学B、C和更多的其他同学都不能打开了。这里我们借助单进程版本来学习。

首先是写出服务器的代码,代码的思路是这样的:

①首先为服务器创建套接字,因为这个是TCP协议,TCP是面向连接的,因此服务器是需要进入监听状态才能让客户端连接,所以使用socket接口创建出来的套接字是属于监听套接字,负责绑定IP和端口号,负责监听的。

②创建完监听套接字后,开始绑定IP和端口号。先创建出服务器的sockaddr_in结构(因为使用AF_INET协议),然后填充结构体,填充的是使用何种协议域,IP和端口号。在填充IP的时候,选择任意绑定IP。

③设置监听状态,监听状态的服务器,通俗地来解释就是服务器进入监听状态,就是告诉客户端我可以被连接了,来吧!

④使用accept接口,创建出提供服务的套接字。这里需要建立另外一个sockaddr_in结构体,这个结构体是保存客户端的ip和端口号。

⑤最后就是提供服务,由于TCP是面向字节流的,跟文件操作一样,因此我们可以使用文件操作进行读写。

注意:

在bind方法中的sockaddr结构体里面填充的是服务端的ip地址和端口号,bind就把服务器的ip地址和端口号和前面的监听套接字结合起来了。而accept方法中的socketaddr结构体保存的是客户端的ip地址和端口号信息。

代码如下:

#include<iostream>
#include<sys/socket.h> 
#include<sys/types.h>
#include<cerrno>
#include<cstring>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<signal.h>
void Usage(std::string proc)
{std::cout<<"Usage: "<<proc<<" port"<<std::endl;
}void ServerceIO(int new_sock)
{while(true){//tcp是面向字节流的,如同文件一样,可以进行正常的读写char buffer[1024];memset(buffer,0,sizeof(buffer));//将读到的数据放入buffer中ssize_t s = read(new_sock,buffer,sizeof(buffer)-1);if(s > 0)//读取成功{buffer[s] = 0;//将获取的内容当成字符串std::cout<<"client#  "<<buffer<<std::endl;std::string echo_string =">>>server<<<, ";echo_string += buffer;write(new_sock,echo_string.c_str(),echo_string.size());}else if(s==0){std::cout<<"client quit..."<<std::endl;break;}   else{std::cerr<<"read err"<<std::endl;break;}}
}// ./tcp_server port
int main(int argc,char *argv[])
{if(argc!=2){Usage(argv[0]);return 1;}//tcp_server//1.创建套接字,此套接字为监听套接字,用于绑定和监听int listen_sock = socket(AF_INET,SOCK_STREAM,0);if(listen_sock < 0){std::cerr<<"socket error"<<std::endl;return 2;}//tcp是流式的,与文件强相关,因此与文件操作接口一起使用//2.bind绑定IP和端口号//填充结构体struct sockaddr_in local;memset(&local,0,sizeof(local));//初始化为0local.sin_family = AF_INET;//协议域local.sin_port =htons(atoi(argv[1])); //端口号local.sin_addr.s_addr = INADDR_ANY;//IP,不能指的IP,需要任意绑定IP;//绑定if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local))<0){std::cerr<<"bind error" <<std::endl;return 3;  }// 3.因为tcp是面向连接的,因此在通信前需要建立连接。UDP是不需要连接的。//建立连接的时候,就一定有人会主动建立连接,有人被动接受连接//而主动建立连接的是客户端,就是需要服务的一方//而被动接受连接的是服务器,就是提供服务的一方//这里的是server,被动接受连接的一方,需要周而复始地不间断地等待客户到来,比如我们半夜打开某app看剧一样//因此作为服务器,需要给用户提供一个建立连接的功能//因此,设置服务器的套接字为Listen监听状态,本质是允许用户连接const int back_log = 5;if(listen(listen_sock,back_log)<0){std::cerr<<"listen error"<<std::endl;return 4;}signal(SIGCHLD,SIG_IGN);//父进程忽略子进程的SIGCHLD信号,子进程会自动退出释放资源//提供服务//使用accept接口,该接口会返回一个套接字,提供服务的套接字for(;;){struct sockaddr_in peer;socklen_t len = sizeof(peer);int new_sock = accept(listen_sock,(struct sockaddr*)&peer,&len);if(new_sock<0){continue;}std::cout<<"get a new link..."<<new_sock<<std::endl;// //单进程版,是没有人会去使用的。// //这里提供服务是死循环,只能对一个客户进行连接,因为其它客户端无法进行循环// //只能串行了ServerceIO(new_sock);}return 0;
}

客户端代码,代码思路如下:

①创建套接字。

②客户端不需要显示绑定ip和端口号。注意,是不需要显示绑定,并非不需要绑定,因为在客户端连接服务器的时候,操作系统会自动地绑定ip和端口号。如果固定地绑定,如果其它客户端随机绑定,随机到了我这个客户端的端口号此时我这个客户端就不能启动了。客户端关心的是连接处于监听状态的服务器。

③发起连接。先创建保存服务器ip地址和端口号信息的socketaddr结构体,然后使用connect方法进行连接。

④连接成功就可以开始通信了。

#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <strings.h>// ./tcp_client server_ip  server_port 
void Usage(std::string proc)
{std::cout<<"Usage: "<<proc<<" server_ip  server_port"<<std::endl;
}
int main(int argc,char *argv[])
{if(argc!=3){Usage(argv[0]);return 1;}//ip和端口号std::string server_ip = argv[1];uint16_t server_port = (uint16_t)(atoi(argv[2]));//1.创建套接字int sock = socket(AF_INET,SOCK_STREAM,0);if(sock<0){std::cerr<<"socket err"<<std::endl;return 2;}//2.绑定bind//客户端需要绑定,但是客户端不需要显示绑定,TCP和UDP都一样,操作系统会自动绑定端口号和ip//因为不能固定地绑,如果其它客户端随机绑定,随机到了我这个客户端的端口号//此时我这个客户端就不能启动了//客户端关心的是连接处于监听状态的服务器//创建服务器的结构并且填充信息struct sockaddr_in server;memset(&server,0,sizeof(server));server.sin_family = AF_INET;//填完,不需要显示绑定,需要连接//inet_addr 该函数做两件事情//1.将点分十进制的字符串风格的ip转化为4字节IP//2. 将四字节IP由主机序列转化为网络序列server.sin_addr.s_addr = inet_addr(server_ip.c_str());server.sin_port = htons(server_port);//发起连接if(connect(sock,(struct sockaddr*)&server,sizeof(server))<0){std::cerr<<"connect err"<<std::endl;return 3;}std::cout<<"connect success!"<<std::endl;//进行业务请求,开始通信while(true){std::cout<<"Please Enter# ";char buffer[1024];//先将数据写到buffer中fgets(buffer,sizeof(buffer)-1,stdin);//然后将buffer中的数据发出去write(sock,buffer,strlen(buffer));//信息返回ssize_t s = read(sock,buffer,sizeof(buffer)-1);if(s>0){buffer[s]=0;std::cout<<"server echo# "<<buffer<<std::endl;}}return 0;
}

多进程版本TCP(客户端/服务器)

由于单进程版没有人会去使用,所以我们通过单进程版本来学习一下简单的代码实现操作,接着我们对单进程版本改造,变成多进程版本。