文章目录
- 简单的TCP网络程序
- 一、服务器创建套接字
- 二、服务器绑定套接字
- 三、服务器监听
- 四、服务器获取连接
- 五、服务器处理请求
- 六、对服务器进行简单测试
- 七、客户端创建套接字
- 八、客户端连接服务器
- 九、客户端发起请求
- 十、服务器客户端测试
- 多进程的TCP服务器
- 一、忽略SIGCHLD信号
- 二、孙子进程提供服务
- 多线程TCP服务器
简单的TCP网络程序
一、服务器创建套接字
与前边的UDP网络程序相同,创建套接字的接口都是socket,下边对socket接口进行介绍:
协议家族选择AF_INET,因为我们要进行网络通信。
而第二个参数,为服务类型,传入SOCK_STREAM,我们编写TCP程序,所以要选择流式的服务。
第三个参数默认传入0,由前两个参数就可以推出这是基于TCP的网络程序。
// 创建套接字_sock = socket(AF_INET, SOCK_STREAM, 0);if (_sock < 0){logMessage(FATAL, "create socket error %d-%s", errno, strerror(errno));exit(2);}
socket接口如果创建成功返回0,失败返回-1,并且错误码被设置,所以当返回值小于0时退出程序。
二、服务器绑定套接字
还是与UDP相同,绑定套接字需要bind接口,我们再次对bind接口进行学习:
第一个参数传入前边创建的套接字,也就是一个文件描述符。
第二个参数是一个sockaddr类型结构体的地址,内边存储着要绑定的IP和端口号的相关信息。
第三个参数为结构体的大小。
// bind绑定套接字struct sockaddr_in local;// bzero((void*)&local,sizeof(local));memset(&local, 0, sizeof local);local.sin_family = AF_INET;local.sin_port = htons(_port);inet_pton(AF_INET, _ip.c_str(), &local.sin_addr);// local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());if (bind(_sock, (struct sockaddr *)&local, (socklen_t)sizeof(local)) < 0){logMessage(FATAL, "bind socket error %d-%s", errno, strerror(errno));exit(3);}
但是同时要注意网络序列和主机序列的转换,并且在处理IP地址时,也要注意到点分十进制与二进制的转换。
三、服务器监听
由于TCP协议是需要连接的,而UDP是不需要连接的,所以在对TCP的服务器进行创建,绑定套接字之后,必须进行监听操作,使服务器处于监听状态。这就例如:
一个商店老板,即使这会没有人来买东西,也必须坐在店里边,处于监听状态,一旦有人来买东西,就可以立马为客户服务。
一旦listen调用成功,服务器就会处于监听状态。
sockfd:
需要设置为监听状态的套接字对应的文件描述符。
backlog:
全连接队列的最大长度。如果有多个客户端同时发来连接请求,此时未被服务器处理的连接就会放入连接队列,该参数代表的就是这个全连接队列的最大长度,一般不要设置太大,设置为5或10即可。
返回值:
如果监听失败返回-1,并且错误码被设置,成功返回0.
// 监听if (listen(_sock, gbacklog) < 0){logMessage(FATAL, "listen socket error %d-%s", errno, strerror(errno));exit(4);}logMessage(NORMAL, "init success,sockfd: %d", _sock);
当监听完成之后,服务器的初始化才算完成。
四、服务器获取连接
当服务器初始化完成之后,此时就要让客户端来连接,必须通过accept来获取连接,当客户端发送连接请求之后,服务器和客户端的连接才正式完成。
参数:
sockfd:监听套接字的文件描述符
addr:对端网络的相关信息结构体,例如IP,端口号,协议家族等
addrlen:addr结构体的大小
返回值:
accept的返回值有一些不同,如果返回成功,这些系统调用返回一个非负整数,它是一个描述符对于接受的套接字。如果出现错误,则返回-1,并适当地设置errno。
那么这个返回值是什么意思呢?为什么会有两个文件描述符,他们之间有什么关系?
当我们使用accpet进行连接时,是通过监听套接字进行连接的,但是当连接上对端网络之后,不是监听套接字来提供服务的,而是返回成功之后,会返回一个套接字的文件描述符,是由该服务套接字提供服务的。
- 监听套接字:用于获取客户端发来的连接请求。accept函数会不断从监听套接字当中获取新连接。
- accept函数返回的套接字:用于为本次accept获取到的连接提供服务。监听套接字的任务只是不断获取新连接,而真正为这些连接提供服务的套接字是accept函数返回的套接字,而不是监听套接字。
下边通过一个例子来解释他们之间的关系:
当我们前往西安旅游时,一定想尝一尝正宗的羊肉泡馍,有一家店,服务员张三非常热情,一定在门口招呼路上的游客进去,当有一个游客准备进入餐馆吃饭时,张三就会喊一声,李四来人了,快出来招呼,但是张三又回到门口,继续让来往的游客进入餐馆,当下一个游客进入餐馆时,张三就说,王五来人了快来招呼人,此时服务顾客的人就是王五,而张三继续去外边找客人。
此处的张三我们就可以认为是监听套接字,主要功能就是不断的在外边找顾客,让顾客进入店内,也就是不断的获取新连接,而后边的李四王五赵六等等就相当于accept返回的服务套接字,他们才是为对端提供服务的。
// 建立连接
struct sockaddr_in src;
socklen_t len = sizeof(src);
int fd = accept(_sock, (struct sockaddr *)&src, &len);
if (fd < 0)
{logMessage(FATAL, "accept error %d-%s", errno, strerror(errno));continue;
}
五、服务器处理请求
通过以上的步骤,创建套接字,绑定套接字,监听,获取连接之后,当客户端对服务器进行连接之后,服务器就可以处理客户端发来的请求。
void service(int fd, const std::string &client_ip, const uint16_t &client_port)
{char buffer[1024];while (1){ssize_t s = read(fd, buffer, sizeof(buffer) - 1);if (s > 0){buffer[s] = 0;std::cout<<client_ip << ":" << client_port << "# " << buffer << std::endl;}else if (s == 0){logMessage(ERROR, "%s-%d client close fd,me too!", client_ip.c_str(), client_port);break;}else{logMessage(FATAL, "read error %d-%s", errno, strerror(errno));break;}write(fd, buffer, strlen(buffer));}close(fd);
}
由于套接字在系统层面来看,就是打开的文件,所以对文件进行读写就可以使用我们之前学习过的 read和write接口。
read
读取数据时,使用read接口。
参数:
fd:文件描述符,表示从哪一个套接字中读取
buf:数据的存储位置,把数据读取到哪一个数组中
count:读取数据的大小
返回值:
当读取成功时,返回读取到的字节数,当写端关闭时,返回0,当读取错误时,返回小于0.
当返回值为0时,表示读取对端关闭了?
网络通信与进程间通信类似,和之前对文件读取写出相同:
- 写端进程不写,读端进程一直读,此时读端进程就会被挂起,因为此时数据没有就绪。
- 读端进程不读,写端进程一直写,此时当缓冲区被写满后写端进程就会被挂起,因为此时空间没有就绪。
- 写端进程将数据写完后将写端关闭,此时当读端进程将管道当中的数据读完后就会读到0。
- 读端进程将读端关闭,此时写端进程就会被操作系统杀掉,因为此时写端进程写入的数据不会被读取。
此处的情况就是写端也就是客户端将数据写完后将写端关闭,此时读端也就是服务器将数据读完之后就会读到0,所以返回值为0。
write
写入数据到网络时,需要使用write接口。
参数:
fd:写端套接字的文件描述符
buf:需要写入的数据
count:需要写入数据的字节数
返回值:
写入成功返回写入的字节数,写入失败返回-1,同时错误码被设置。
六、对服务器进行简单测试
当服务器初始化已经处理请求都完成之后,虽然还没有实现客户端,但是也可以telnet
指令远程连接该服务器,实现请求处理服务:
第一步:
运行服务器,必须加上端口号,此时处于监听状态,等待客户端连接。
此时可以使用netstat
指令观察该套接字的状态:
可以发现此时的服务器处于listen状态。
第二步:
使用telnet指令对服务器进行连接。
第三步:
对服务器进行请求。
七、客户端创建套接字
与前边创建套接字没有什么区别,注意的就是使用流式传输。
客户端是不需要绑定IP和端口号的,在客户端在连接时,系统会自动给客户端分配。
客户端也不需要监听,因为客户端不会被主动连接。
int main(int argc, char *argv[])
{if (argc != 3){usage(argv[0]);exit(1);}std::string server_ip = argv[1];uint16_t server_port = atoi(argv[2]);// 创建套接字int sock = socket(AF_INET, SOCK_STREAM, 0);if (sock < 0){std::cerr << "socket error" << std::endl;exit(2);}
}
八、客户端连接服务器
客户端要发送请求时,必须要知道服务器的IP地址和端口号,所以我们使用命令行参数的方式,将服务器的Ip地址和端口号传给客户端,客户端接收之后之后,将IP地址和端口号传入addr结构体中,然后使用connect接口进行连接。
struct sockaddr_in peer;memset(&peer,'\0', sizeof(peer));peer.sin_family = AF_INET;peer.sin_port = htons(server_port);peer.sin_addr.s_addr = inet_addr(server_ip.c_str());// 连接套接字if (connect(sock, (struct sockaddr *)&peer, (socklen_t)sizeof(peer)) < 0){std::cerr << "connect error" << std::endl;exit(3);}std::cout << "connect success" << std::endl;
connect接口如果调用成功,客户端会被随机分配一下端口号,只要可以唯一标识客户端即可。
九、客户端发起请求
客户端与服务器连接成功之后,使用send接口发送数据,如果发送成功,返回值大于0。当发送成功之后,使用recv接口接收数据,最后在收到的数据后加上’\0’,将字符串回显。
while (true){std::string line;std::cout << "请输入# " << std::endl;getline(std::cin, line);if (line == "quit")break;ssize_t s = send(sock,line.c_str(),line.size(),0);if(s>0){char buffer[1024];ssize_t s = recv(sock,buffer,sizeof(buffer)-1,0);if(s > 0){buffer[s]=0;std::cout<<"回显#"<<buffer<<std::endl;}else if(s==0){break;}else{break;}}}
十、服务器客户端测试
服务端
客户端
当在客户端发起请求之后,客户端会与服务器建立连接,此时再次输入数据,服务器会对收到的数据进行回显。
多进程的TCP服务器
如果是单进程的服务器,当多个客户端同时启动,服务器只能处理一个客户端的请求,只有当第一个客户端退出之后,才会收到第二个客户端的请求。
为什么可以使用多进程
由于创建子进程后,子进程会继承父进程的文件描述符等信息,所以父进程创建的套接字也会被子进程继承下来,当我们使用多进程时,子进程就可以看到建立链接的文件描述符,并且当某一个进程处理完毕之后关闭文件描述符,也不会影响到其他的进程,因为父子进程具有独立性,在修改时会进行写时拷贝。
等待子进程问题
在子进程处理请求完毕之后,父进程必须等待子进程,要不然就会造成僵尸问题,会造成内存泄露,等待子进程有两种方式:
- 阻塞等待
- 非阻塞等待
如果使用阻塞等待,那么说明父进程必须在等待第一个子进程服务完毕之后才可以处理下一个请求,本质上还是进行串行操作,并没有真正实现多进程。
而如果使用非阻塞等待,虽然可以再进行其他的连接,但是必须不断的检测子进程是否退出。
为了解决以上的问题,我们可以采取两种方法:
- 对SIGCHLD进行自定义捕捉,主动忽略SIGCHLD信号,当子进程退出时,就会主动释放僵尸进程,父进程不会进行等待。
- 创建子进程,再让子进程创建子进程,让孙子进程进行服务,将子进程直接退出,当孙子进程处理完毕之后,成为孤儿进程被操作系统回收,所以父进程不需要进行等待。
一、忽略SIGCHLD信号
signal(SIGCHLD, SIG_IGN); // 对SIGCHLD,主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态.//version1.0多进程版,对信号进行忽略pid_t id = fork();assert(id != -1);if (id == 0){close(_sock);service(fd, client_ip, client_port);exit(0);}close(fd);
二、孙子进程提供服务
先创建子进程,再让子进程创建子进程,让孙子进程提供服务,但是将子进程退出,当孙子进程提供完服务之后,被操作系统回收。
void service(int fd, const std::string &client_ip,const uint16_t &client_port)
{char buffer[1024];while (1){ssize_t s = read(fd, buffer, sizeof(buffer) - 1);if (s > 0){buffer[s] = 0;std::cout<<client_ip << ":" << client_port << "# " << buffer << std::endl;}else if (s == 0){logMessage(ERROR, "%s-%d client close fd,me too!", client_ip.c_str(), client_port);break;}else{logMessage(FATAL, "read error %d-%s", errno, strerror(errno));break;}write(fd, buffer, strlen(buffer));}close(fd);
}
//version1 .1多进程版,使用孙子进程进行服务
pid_t id = fork();
if (id == 0)
{close(_sock);if (fork() > 0)exit(0);else{service(fd, client_ip, client_port);exit(0);}
}
waitpid(id, nullptr, 0);
close(fd);
多线程TCP服务器
服务器为了同时给多个客户端提供服务,不仅可以使用多进程来进行服务,也可以使用多线程来提供服务。
由于线程的回调函数中需要多个变量,所以我们将需要的IP,端口号,文件描述符写入一个类中,将实例化的对象指针传入回调函数,在回调函数中使用pthread_detach接口实现线程分离,主线程这边就不需要进行join回收线程了。
//服务函数
void service(int fd, const std::string &client_ip,const uint16_t &client_port)
{char buffer[1024];while (1){ssize_t s = read(fd, buffer, sizeof(buffer) - 1);if (s > 0){buffer[s] = 0;std::cout << client_ip << ":" << client_port << "# " << buffer << std::endl;}else if (s == 0){logMessage(ERROR, "%s-%d client close fd,me too!", client_ip.c_str(), client_port);break;}else{logMessage(FATAL, "read error %d-%s", errno, strerror(errno));break;}write(fd, buffer, strlen(buffer));}close(fd);
}//线程数据
class pthreadData
{
public:int _sock;std::string _ip;uint16_t _port;
};// version2多线程版
pthreadData *pd = new pthreadData();
pd->_port = client_port;
pd->_sock = fd;
pthread_t tid;
pthread_create(&tid, nullptr, Routine, pd);//线程回调函数
static void *Routine(void *args)
{pthread_detach(pthread_self());pthreadData *pd = (pthreadData *)args;std::string client_ip = pd->_ip;uint16_t client_port = pd->_port;int sock = pd->_sock;service(sock, client_ip, client_port);return nullptr;
}