目录
服务器端
初始化
服务器启动
测试服务器
客户端
代码改进
1、多进程版本
2、多线程版本
三种版本的比较
之前有讲过基于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!"<
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"<
当获得到连接之后,需要对连接进行处理,自定义一下处理动作
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<
其中有两个函数,一个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!"<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!"<
初始化完成后进行通信,这里就比较简单,自定义函数。
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: "<
主函数
int main(int argc,char* argv[])
{
if(argc != 3)
{
cerr<<"parameter error!"<initClient();
uc->start();
return 0;
}
运行结果
上述程序面临的一些问题
面对多个请求如何实现?
只针对服务器
修改一下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"<
这里有很多细节
1、由于子进程会按照父进程的模板来创建,所以文件描述符资源也对应相同,子进程需要关闭一些自己并不关心的文件描述符资源,父进程也是如此。
2、子进程的退出,子进程的资源需要回收,此时需要父进程来处理(注意,不是父进程回收,父进程只是发起回收这个动作,实质上是内核完成资源的回收)。一般来说父进程需要等待(wait/waitpid)。还有一些其他的方法,比如说自定义捕捉(针对SIGCHLD信号),或者直接忽略,将资源交给内核回收。
忽略比较简单,在初始化那里加一行代码 signal(SIGCHLD,SIG_IGN);
测试:
同时有三个连接请求,将前两个进程放在后台
查看当前进程信息,三个客户端Client进程,三个子进程Server在处理连接,一个父进程Server。
这样服务器就可以同时应对多个连接。
但是多进程的方式资源消耗比较大,且进程间的切换开销也很大。引入多线程版本。
static void* service_routine(void* arg)
{
pthread_detach(pthread_self());//分离线程,避免主线程阻塞等待释放资源
cout<<"creat thread successfully,tid is "<
需要注意的几个点
1、由于在类里面,线程处理的函数得是静态函数,因为函数参数这里有一个隐藏的this指针,所以可以处理成静态函数的方式舍弃this指针,由于静态函数没有this指针,所以也无法调用类里面的service函数,此时也要把service设置为静态函数。
2、线程退出时也需要主线程释放资源,如果用pthread_join函数去释放资源,主线程会陷入阻塞状态,在上一个线程为退出的状态下,无法接收新的连接,所以可以采用分离线程的方式。
1、单进程:一般不使用
2、多进程版本:健壮性强,比较吃资源,效率低下
3、多线程版本:健壮性不强,较吃资源,效率相对较高
当大量客户端需要接入时,系统会存在大量的执行流。此时切换是影响效率的重要原因