【在Linux世界中追寻伟大的One Piece】Socket编程TCP

时间:2024-11-01 07:18:01

目录

1 -> TCP socket API

2 -> V1 -Echo Server

2.1 -> 测试多个连接的情况


1 -> TCP socket API

socket():

  • socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符。
  • 应用程序可以像读写文件一样用read/write在网络上收发数据。
  • 如果socket()调用出错则返回-1。
  • 对于IPv4,family参数指定为AF_INET。
  • 对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议。
  • protocol参数的介绍从略,指定为0即可。

bind():

  • 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接;服务器需要调用bind绑定一个固定的网络地址和端口号。
  • bind()成功返回0,失败返回-1。
  • bind()的作用是将参数sockfd和myaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号。
  • struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。

我们的程序中对myaddr参数是这样初始化的:

  1. 将整个结构体清零。
  2. 设置地址类型为AF_INET。
  3. 网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址。
  4. 端口号为SERV_PORT,定义为9999。

listen():

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

accept():

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

我们的服务器程序结构是这样的:

connect

  • 客户端需要调用connect()连接服务器。
  • connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。
  • connect()成功返回0,出错返回-1。

2 -> V1 -Echo Server

nocopy.hpp

#pragma once
#include <iostream>

class nocopy
{
public:
	nocopy() {}

	nocopy(const nocopy&) = delete;
	const nocopy& operator = (const nocopy&) = delete;

	~nocopy() {}
};

TcpServer.hpp

#pragma once
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "nocopy.hpp"
#include "Comm.hpp"

const static int default_backlog = 6; // TODO
class TcpServer : public nocopy
{
public:
	TcpServer(uint16_t port) : _port(port), _isrunning(false)
	{
	}

	// 都是固定套路
	void Init()
	{
		// 1. 创建 socket, file fd, 本质是文件
		_listensock = socket(AF_INET, SOCK_STREAM, 0);
		if (_listensock < 0)
		{
			lg.LogMessage(Fatal, "create socket error, errnocode: % d, error string : % s\n", errno, strerror(errno));
			exit(Fatal);
		}

		int opt = 1;
		setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR |
		SO_REUSEPORT, &opt, sizeof(opt));

		lg.LogMessage(Debug, "create socket success,
			sockfd: % d\n", _listensock);

		// 2. 填充本地网络信息并 bind
		struct sockaddr_in local;
		memset(&local, 0, sizeof(local));
		local.sin_family = AF_INET;
		local.sin_port = htons(_port);
		local.sin_addr.s_addr = htonl(INADDR_ANY);

		// 2.1 bind
		if (bind(_listensock, CONV(&local), sizeof(local)) != 0)
		{
			lg.LogMessage(Fatal, "bind socket error, errno
				code: % d, error string : % s\n", errno, strerror(errno));
				exit(Bind_Err);
		}

		lg.LogMessage(Debug, "bind socket success, sockfd: %d\n",
			_listensock);

		// 3. 设置 socket 为监听状态,tcp 特有的
		if (listen(_listensock, default_backlog) != 0)
		{
			lg.LogMessage(Fatal, "listen socket error, errno
				code: % d, error string : % s\n", errno, strerror(errno));
				exit(Listen_Err);
		}
		lg.LogMessage(Debug, "listen socket success,
			sockfd: % d\n", _listensock);
	}

	// Tcp 连接全双工通信的.
	void Service(int sockfd)
	{
		char buffer[1024];
		// 一直进行 IO
		while (true)
		{
			ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
			if (n > 0)
			{
				buffer[n] = 0;
				std::cout << "client say# " << buffer <<
					std::endl;

				std::string echo_string = "server echo# ";
				echo_string += buffer;
				write(sockfd, echo_string.c_str(),
					echo_string.size());
			}
			else if (n == 0) // read 如果返回值是 0,表示读到了文件结尾(对端关闭了连接!)
			{
				lg.LogMessage(Info, "client quit...\n");

				break;
			}
			else
			{
				lg.LogMessage(Error, "read socket error, errno
					code: % d, error string : % s\n", errno, strerror(errno));
					break;
			}
		}
	}

	void Start()
	{
		_isrunning = true;
		while (_isrunning)
		{
			// 4. 获取连接
			struct sockaddr_in peer;
			socklen_t len = sizeof(peer);

			int sockfd = accept(_listensock, CONV(&peer), &len);
			if (sockfd < 0)
			{
				lg.LogMessage(Warning, "accept socket error, errno
					code: % d, error string : % s\n", errno, strerror(errno));
					continue;
			}

			lg.LogMessage(Debug, "accept success, get n new
			sockfd: % d\n", sockfd);
			// 5. 提供服务, v1~v4
			// v1
			// Service(sockfd);
			close(sockfd);
		}
	}

	~TcpServer()
	{
	}
private:
	uint16_t _port;
	int _listensock; // TODO
	bool _isrunning;
};

Comm.hpp

#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

enum 
{
	Usage_Err = 1,
	Socket_Err,
	Bind_Err,
	Listen_Err
};

#define CONV(addr_ptr) ((struct sockaddr *)addr_ptr)

TcpClient.hpp

#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Comm.hpp"
using namespace std;

void Usage(const std::string& process)
{
	std::cout << "Usage: " << process << " server_ip server_port"
		<< std::endl;
}

// ./tcp_client serverip serverport
int main(int argc, char* argv[])
{
	if (argc != 3)
	{
		Usage(argv[0]);

		return 1;
	}

	std::string serverip = argv[1];
	uint16_t serverport = stoi(argv[2]);

	// 1. 创建 socket
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (sockfd < 0)
	{
		cerr << "socket error" << endl;

		return 1;
	}

	// 2. 要不要 bind?必须要有 Ip 和 Port, 需要 bind,但是不需要用户显示的 bind,client 系统随机端口
	// 发起连接的时候,client 会被 OS 自动进行本地绑定
	// 
	// 2. connect
	struct sockaddr_in server;
	memset(&server, 0, sizeof(server));
	server.sin_family = AF_INET;
	server.sin_port = htons(serverport);

	// p:process(进程), n(网络) -- 不太准确,但是好记忆
	inet_pton(AF_INET, serverip.c_str(), &server.sin_addr); // 1. 字符串 ip->4 字节 IP 2. 网络序列
	int n = connect(sockfd, CONV(&server), sizeof(server)); // 自动进行 bind 哦!
	if (n < 0)
	{
		cerr << "connect error" << endl;

		return 2;
	}

	// 并没有向 server 一样,产生新的 sockfd.未来我们就用 connect 成功的sockfd 进行通信即可.
	while (true)
	{
		string inbuffer;
		cout << "Please Enter# ";
		getline(cin, inbuffer);
		ssize_t n = write(sockfd, inbuffer.c_str(),
			inbuffer.size());
		if (n > 0)
		{
			char buffer[1024];
			ssize_t m = read(sockfd, buffer, sizeof(buffer) - 1);
			if (m > 0)
			{
				buffer[m] = 0;
				cout << "get a echo messsge -> " << buffer <<
					endl;
			}
			else if (m == 0 || m < 0)
			{
				break;
			}
		}
		else
		{
			break;
		}
	}

	close(sockfd);

	return 0;
}

由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配。

注意:

  • 客户端不是不允许调用bind(),只是没有必要显示的调用bind()固定一个端口号。否则如果在同一台机器上启动多个客户端,就会出现端口号被占用导致不能正确建立连接。
  • 服务器也不是必须调用bind(),但如果服务器不调用bind(),内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。

2.1 -> 测试多个连接的情况

再启动一个客户端,尝试连接服务器,发现第二个客户端,不能正确的和服务器进行通信。

分析原因,是因为我们accecpt了一个请求之后,就在一直while循环尝试read,没有继续调用到 accecpt,导致不能接受新的请求。

我们当前的这个TCP,只能处理一个连接,这是不科学的。


感谢各位大佬支持!!!

互三啦!!!