Linux操作系统 - 网络编程socket
Posted TangguTae
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux操作系统 - 网络编程socket相关的知识,希望对你有一定的参考价值。
目录
之前有讲过基于UDP的网络编程一些基础的知识,现在看看基于TCP的网络编程。
首先TCP与UDP最大的不同是TCP是面向连接的,可靠传输。所以在编程实现方面有很多不同的地方,接下来看看具体的细节。
服务器端
在初始化的过程中,与UDP的方式有很多相同的地方,例如创建套接字,绑定端口号。而在TCP除了上述的操作以外还需要进行监听,即把套接字变成监听套接字。
初始化
class tcpServer
private:
int _port;
int _lsock;
public:
tcpServer(int port = 8080)
:_port(port)
//初始化
void initServer()
_lsock = socket(AF_INET,SOCK_STREAM,0);//创建套接字
if(_lsock < 0)
cerr<<"sock error!"<<endl;
exit(1);
sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
if(bind(_lsock,(struct sockaddr*)&local,sizeof(local)) < 0)//绑定端口和ip地址
cerr<<"bind error!"<<endl;
exit(2);
//进行监听
if(listen(_lsock,5) < 0)//队列长度设置为5(实际底层大小为6)
cerr<<"listen error!"<<endl;
exit(3);
;
listen 函数
listen函数将套接字变为监听套接字,第一个参数就是传递创建好的套接字,第二个参数使用来确定最大接收连接的数量,这个地方涉及到全连接队列的知识,在后面说TCP协议的细节的时候在说一下这个参数,本身的目的是让系统效率最大化,不过这个参数不能太大。
服务器启动
当服务器启动之后需要接收来自客户端的连接。
accept函数
第一个参数是监听套接字(listen过后的套接字),第二个参数、第三个参数是输出型参数。
返回值
注意:返回值是一个文件描述符,也是一个socket,很关键,这个文件描述符就是实际通信过程中所需的文件描述符。
如果队列上不存在挂起的连接,并且套接字未标记为非阻塞,则accept()会阻塞调用方,直到存在连接为止。如果套接字标记为非阻塞,并且队列中不存在挂起的连接,则accept()将失败,并出现错误EAGAIN或EWOLDBLOCK。
void start()
sockaddr_in remote;
socklen_t len = sizeof(remote);
while(true)
int sock = accept(_lsock,(struct sockaddr*)&remote,&len);
if(sock < 0)
cerr<<"accept error"<<endl;
continue;//这里失败不退出,继续等待连接
cout<<"get a new link"<<endl;
service(sock);//对连接进行处理
当获得到连接之后,需要对连接进行处理,自定义一下处理动作
void service(int sock)
char buf[1024];
while(true)
ssize_t s = recv(sock,buf,sizeof(buf)-1,0);
if(s > 0)
buf[s] = '\\0';
cout<<buf<<endl;//对收到的信息打印到命令行
string echo(buf);
echo+=" echo Server";
send(sock,echo.c_str(),echo.size(),0);//返回给客户端
else if(s == 0)//读到0表示客户端已经退出
cout<<"client is quit!"<<endl;
break;
else
cout<<"recv error!"<<endl;
close(sock);//记得结束后关闭文件描述符
其中有两个函数,一个recv函数,一个是send函数。
recv函数
在UDP里面讲过一个recvfrom函数,recvfrom函数针对UDP,需要告知是谁发过来的,有一个输出型的参数,而recv没有,因为TCP在接收连接(accept)的时候已经知道是谁发过来的,所以可以不用recvfrom函数。
参数分别是套接字、接收数据buf、期望接收的大小和阻塞接收标志flag。flag=0表示阻塞。
返回值
返回值为实际读到多少字节的数据,读到0表示对方已经断开连接,读到-1表示出错。
send函数
和sendto是一类接口,不过send是面向TCP的,不需要指名发给谁,因为TCP已经把连接建立好了,文件描述符在底层绑定了对方的ip地址和端口号。
可以发现这一批接口和系统IO中的read和write很相似,此时这两组接口都是面向字节流的(TCP就是面向字节流的),也就是说我们用read和write也可以直接读取套接字里面的数据或者往套接字里面写入数据。
int main(int argc,char* argv[])
if(argc != 2)
cerr<<"parameter error!"<<endl;
exit(1);
tcpServer *us = new tcpServer(atoi(argv[1]));
us->initServer();
us->start();
return 0;
测试服务器
运行一下
补充一个工具telnet
telnet是远程终端协议,是TCP/IP协议家族的成员之一,默认端口23。用来测试网络。
用telnet工具来作为客户端测试一下服务器。
首先需要安装一下:sudo yum install telnet telnet-server
退出就是在telnet命令模式下ctrl ] 输入quit
客户端
与UDP不同的是,客户端需要发起连接
connect函数
第一个参数是文件描述符,第二个第三个参数想必已经不陌生了,需要对方的ip地址和端口号信息,以结构体的形式传递参数。
class tcpClient
private:
string _ip;
int _port;
int sock;
public:
tcpClient(string ip = "127.0.0.1",int port = 8080)
:_ip(ip)
,_port(port)
void initClient()
sock = socket(AF_INET,SOCK_STREAM,0);//SOCK_STREAM面向字节流
if(sock < 0)
cerr<<"sock error!"<<endl;
exit(1);
struct sockaddr_in remote;//绑定目的ip地址与端口号
remote.sin_family = AF_INET;
remote.sin_port = htons(_port);
remote.sin_addr.s_addr = inet_addr(_ip.c_str());
if(connect(sock,(struct sockaddr*)&remote,sizeof(remote)) != 0)//连接请求
cerr<<"connect error!"<<endl;
exit(2);
;
初始化完成后进行通信,这里就比较简单,自定义函数。
void start()
while(true)
string msg;
cout<<"please enter msg# ";
cin>>msg;
send(sock,msg.c_str(),msg.size(),0);
char echo[128] = '\\0';
ssize_t s = recv(sock,echo,sizeof(echo)-1,0);
if(s > 0)
echo[s] = '\\0';
cout<<"get msg from Server: "<<echo<<endl;
主函数
int main(int argc,char* argv[])
if(argc != 3)
cerr<<"parameter error!"<<endl;
exit(1);
tcpClient *uc = new tcpClient(argv[1],atoi(argv[2]));
uc->initClient();
uc->start();
return 0;
运行结果
上述程序面临的一些问题
面对多个请求如何实现?
代码改进
只针对服务器
1、多进程版本
修改一下start函数
void start()
sockaddr_in remote;
socklen_t len = sizeof(remote);
while(true)
int sock = accept(_lsock,(struct sockaddr*)&remote,&len);
if(sock < 0)
cerr<<"accept error\\n"<<endl;
continue;
pid_t id = fork();
if(id == 0)
close(_lsock); //子进程不关心监听套接字,直接关闭
service(sock);
exit(0);
close(sock);//由于交给子进程处理,父进程不需要关心sock
cout<<"get a new link"<<endl;
//子进程退出,一些方法回收资源
//1、父进程等待(不能阻塞等待)
//2、自定义捕捉信号SIGCHLD
//3、将SIGCHLD信号忽略
这里有很多细节
1、由于子进程会按照父进程的模板来创建,所以文件描述符资源也对应相同,子进程需要关闭一些自己并不关心的文件描述符资源,父进程也是如此。
2、子进程的退出,子进程的资源需要回收,此时需要父进程来处理(注意,不是父进程回收,父进程只是发起回收这个动作,实质上是内核完成资源的回收)。一般来说父进程需要等待(wait/waitpid)。还有一些其他的方法,比如说自定义捕捉(针对SIGCHLD信号),或者直接忽略,将资源交给内核回收。
忽略比较简单,在初始化那里加一行代码 signal(SIGCHLD,SIG_IGN);
测试:
同时有三个连接请求,将前两个进程放在后台
查看当前进程信息,三个客户端Client进程,三个子进程Server在处理连接,一个父进程Server。
这样服务器就可以同时应对多个连接。
但是多进程的方式资源消耗比较大,且进程间的切换开销也很大。引入多线程版本。
2、多线程版本
static void* service_routine(void* arg)
pthread_detach(pthread_self());//分离线程,避免主线程阻塞等待释放资源
cout<<"creat thread successfully,tid is "<<pthread_self()<<endl;
int *p = (int*)arg;
service(*p);//此时service也需要是静态函数
return nullptr;
void start()
sockaddr_in remote;
socklen_t len = sizeof(remote);
while(true)
int sock = accept(_lsock,(struct sockaddr*)&remote,&len);
if(sock < 0)
cerr<<"accept error\\n"<<endl;
continue;
cout<<"get a new link"<<endl;
pthread_t tid;
pthread_create(&tid,nullptr,service_routine,(void*)&sock);//创建线程去执行通信
需要注意的几个点
1、由于在类里面,线程处理的函数得是静态函数,因为函数参数这里有一个隐藏的this指针,所以可以处理成静态函数的方式舍弃this指针,由于静态函数没有this指针,所以也无法调用类里面的service函数,此时也要把service设置为静态函数。
2、线程退出时也需要主线程释放资源,如果用pthread_join函数去释放资源,主线程会陷入阻塞状态,在上一个线程为退出的状态下,无法接收新的连接,所以可以采用分离线程的方式。
三种版本的比较
1、单进程:一般不使用
2、多进程版本:健壮性强,比较吃资源,效率低下
3、多线程版本:健壮性不强,较吃资源,效率相对较高
当大量客户端需要接入时,系统会存在大量的执行流。此时切换是影响效率的重要原因
以上是关于Linux操作系统 - 网络编程socket的主要内容,如果未能解决你的问题,请参考以下文章