【Linux】之 TCP服务器与客户端

一、TCP服务器的特点

面向连接,可靠传输,字节流服务(适用安全性高,信息质量要求高的传输:文件传输)

先连接再传输。 数据传输灵活。

缺点:传输速度较低,数据可能粘包。

二、TCP服务器的建立流程

socket -> bind -> listen -> connect -> accept -> recv/send -> close

可以看出TCP连接的建立比起UDP多了三步分别是 listen(监听),connect(请求连接),accept(接受连接)

#三次握手过程

那我们就先看一下man手册中是怎么说的#

listen():

listen接口的描述中贴心的讲解了TCP连接的建立步骤:                             #“男人”真是贴心啊,问一答三可还行/xyx

首先,使用套接字(2)创建套接字。

接下来,一个接收传入连接的意愿和传入连接的队列限制由listen()指定。

最后,使用accept(2)接受连接。listen()调用只应用于SOCK_STREAM类型的套接字。

  • listen()声明sockfd处于监听状态, 并且最多允许有backlog个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略, 这里设置不会太大。
  • 调用成功返回0,失败返回-1

connect():

如果套接字类型为SOCK_STREAM(即流式套接字),则此调用将尝试连接到另一个套接字。另一个套接字由地址指定,地址是套接字通信空间中的地址。

通常,流套接字可以成功connect()只有一次;数据报套接字可以多次使用connect()来更改它们的关联。

  • connect和bind的参数形式一致, 区别在于bind的参数是自己的地址, 而connect的参数是对方的地址;
  • connect()成功返回0,出错返回-1;

accept():

accept的描述篇幅很长,这里就不贴了,总结一下是下面几点:

  • 三次握手完成后, 服务器调用accept()接受连接;
  • 如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;
  • addr是一个传出参数,accept()返回时传出客户端的地址和端口号;
  • 如果给addr 参数传NULL,表示不关心客户端的地址;
  • addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度 以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区);

注意这个坑!

man:If no pending connections are present on the queue, and the socket is not marked as non-blocking, accept() blocks the caller until a connection is present.  

我第一次编写TCP服务器时,就栽到这里了。意思是说如果,如果没有新连接建立,这个函数会一直阻塞在这里等待新连接。

这就是说,如果不进行特殊处理的话,调用accept接口,将会阻塞你的服务器与客户端间的交互。

下面贴上我的接口封装和我栽倒的V0版服务器。

#ifndef __M_TCPSOCK_H__
#define __M_TCPSOCK_H__

#include<sys/wait.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<iostream>
#include<arpa/inet.h>

#define CHECK_RET(q) if ((q) == false) {return -1;}
class TcpSocket
{
	private:
		int _sockfd;
	public:
		TcpSocket():_sockfd(-1){}
        	//socket
		bool CreateSock()
		{
			_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
			if(_sockfd < 0)
			{
				perror("Socket Create Failed!");
				return false;
			}
			return true;
		}
		//bind
		bool BindAddr(std::string &ip, uint16_t port)
		{
			struct sockaddr_in addr;
			addr.sin_family = AF_INET;
			addr.sin_port = htons(port);
			addr.sin_addr.s_addr = inet_addr(ip.c_str());
			socklen_t len = sizeof(struct sockaddr_in);
			int ret = bind(_sockfd, (struct sockaddr*)&addr, len);
			if(ret < 0)
			{
				perror("Bind Failed!");
				return false;
			}
			return true;
		}
		//connect
		bool Connect(std::string &ip, uint16_t port)
		{
			struct sockaddr_in addr;
			addr.sin_family = AF_INET;
			addr.sin_port = htons(port);
			addr.sin_addr.s_addr = inet_addr(ip.c_str());
			socklen_t len = sizeof(struct sockaddr_in);
			int ret = connect(_sockfd, (struct sockaddr*)&addr, len);
			if(ret < 0)
			{
				perror("connect failed");
				return false;
			}
			return true;
		}
		//listen
		bool StartListen(int max = 5)
		{
			int ret = listen(_sockfd, max);
			if(ret < 0)
			{
				perror("Listen error");
				return false;
			}
			return true;
		}
		//accept
		bool Accept(TcpSocket *sock, sockaddr_in *cliaddr = NULL)
		{
			socklen_t len = sizeof(sockaddr_in);
			sockaddr_in addr;
			int newsockfd = accept(_sockfd, (sockaddr*)&addr, &len);
			if(newsockfd < 0)
			{
				perror("accept error");
				return false;
			}
			sock->_sockfd = newsockfd;
			if(cliaddr != NULL)
				memcpy(cliaddr, &addr, len);
			return true;
		}
		//recv
		ssize_t Recv(char* buf)
		{
			ssize_t ret = recv(_sockfd, buf, 1023, 0);
			if(ret < 0)
			{
				perror("recv error");
				return -1;
			}else if(ret == 0)
			{
				printf("peer shutdown\n");
			}
			return ret;
		}
		//send
		ssize_t Send(const char* buf)
		{
			size_t len = strlen(buf);
			ssize_t ret = send(_sockfd, buf, len, 0);
			if(ret < 0)
			{
				perror("send error");
				return -1;
			}
			return ret;
		}
		//close
		bool Close()
		{
			close(_sockfd);
			_sockfd = -1;
			return true;
		}

};
#endif
//tcp_ser_V0
#include "TCPSOCK.h"

int main(int argc, char* argv[])
{
	if (argc != 3) {
        printf("Usage: ./tcp_srv ip port\n");
        return -1;
    }
    std::string ip = argv[1];
    uint16_t port = atoi(argv[2]);

	TcpSocket sock;
	CHECK_RET(sock.CreateSock());
	CHECK_RET(sock.BindAddr(ip, port));
	CHECK_RET(sock.StartListen(5));

	while(1){
		TcpSocket clisock;
		if(sock.Accept(&clisock) == false){
			continue;
		}
		char buf[1024] = {0};
		clisock.Recv(buf);
		printf("cli:%s\n",buf);
		memset(buf, 0x00, 1024);
		printf("ser:");
		//fflush(stdout);
		scanf("%s", buf);
		clisock.Send(buf);
	}
	sock.Close();
	return 0;
}

这个V0版的服务端写法存在的问题就是;一个连接建立后,处理完一波请求之后,程序被阻塞在accept(),就无法再处理当前客户端的请求。

所以,我提出了两种解决方案。一种是通过进程创建,创建一个子进程,用这个子进程创建一个处理连接请求的孙子进程。

子进程创建完孙子进程之后直接退出,目的是让孙子进程成为孤儿进程,能够在处理完连接请求之后自己退出。

改动如下:

//tcp_ser_VProc
while(1){
	TcpSocket clisock;
	if(sock.Accept(&clisock) == false){
		continue;
	}
	if(fork() == 0){
		if(fork() == 0){
			while(1){
				char buf[1024] = {0};
				clisock.Recv(buf);
				printf("cli:%s\n",buf);
				memset(buf, 0x00, 1024);
				printf("ser:");
				//fflush(stdout);
				scanf("%s", buf);
				clisock.Send(buf);
			}
			clisock.Close();
		}else{
			exit(0);
		}
	}
	clisock.Close();
	wait(NULL);
}

还有一种是通过多线程的方式,每一个连接为其创建一个线程去处理,改动如下:

//tcp_ser_VThread
void *thr_start(void *arg)
{
	TcpSocket *clisock = (TcpSocket*)arg;
	while(1){
		char buf[1024] = {0};
		clisock->Recv(buf);
		printf("cli:%s\n",buf);
		memset(buf, 0x00, 1024);
		printf("ser:");
		//fflush(stdout);
		scanf("%s", buf);
		clisock->Send(buf);
	}
	clisock->Close();
	delete clisock;
	return NULL;
}

while(1){
	TcpSocket *clisock = new TcpSocket();
	if(sock.Accept(clisock) == false){
		continue;
	}
	pthread_t tid;
	pthread_create(&tid, NULL, thr_start, (void*)clisock);
	pthread_detach(tid);
}

PS:还有线程池实现的版本,改日重开一贴详细分析。

以及三次握手,四次挥手的过程,也会重开一贴专门分析。

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值