TCP IP 네트워크 프로그래밍 (4) TCP 기반 서버 및 클라이언트

TCP, UDP 이해

TCP/IP 프로토콜 스택

​TCP /IP 프로토콜 스택

이미지 설명을 추가해주세요

TCP/IP 프로토콜 스택은 4개의 계층으로 나누어져 있는데, 데이터 송수신이 4개의 계층적 프로세스로 나누어져 있음을 이해할 수 있습니다.

​TCP 프로토콜 스택

여기에 이미지 설명을 삽입하세요.

UDP 프로토콜 스택

여기에 이미지 설명을 삽입하세요.

링크 레이어

링크 계층은 물리적 연결 분야의 표준화의 결과물이자 LAN, WAN, MAN 등의 네트워크 표준을 구체적으로 정의하는 가장 기본적인 분야이기도 하다. 두 호스트가 네트워크를 통해 데이터를 교환하기 위해서는 아래 그림과 같이 물리적 연결이 필요하며, 이러한 표준을 담당하는 것이 링크 계층이다.

여기에 이미지 설명을 삽입하세요.

IP 계층

IP 프로토콜은 메시지 지향적이고 신뢰할 수 없는 프로토콜입니다. 데이터를 전송할 때마다 경로를 선택하는 데 도움이 되지만 일관성이 없습니다. 전송 중에 경로 오류가 발생하면 다른 경로가 선택됩니다. 그러나 데이터 손실이나 오류가 발생하면 해결할 수 없습니다. IP 프로토콜은 데이터 오류에 대처할 수 없습니다.

TCP/UDP 계층

IP 계층은 데이터 전송 시 경로 선택 문제를 해결하며 이 경로를 통해서만 데이터를 전송하면 됩니다. TCP 및 UDP 계층은 IP 계층에서 제공하는 경로 정보를 기반으로 실제 데이터 전송을 완료하므로 이 계층을 전송 계층이라고도 합니다. TCP는 안정적인 데이터 전송을 보장하지만 IP 계층을 기반으로 데이터를 보냅니다.

여기에 이미지 설명을 삽입하세요.

TCP와 UDP는 IP 계층 상위에 존재하며 호스트 간의 데이터 전송 방법을 결정하며, TCP 프로토콜은 신뢰할 수 없는 IP 프로토콜에 확인 후 신뢰성을 부여합니다.

애플리케이션 레이어

위 클래스는 소켓 통신 중에 자동으로 처리됩니다. 데이터 전송 경로 선택과 데이터 확인 과정은 소켓 내부에 숨겨져 있습니다. 그러나 이러한 이론을 숙지해야만 우리의 요구 사항을 충족하는 네트워크 프로그램을 작성할 수 있습니다.

모든 사람에게 제공되는 도구는 소켓이며, 누구나 소켓을 사용하여 프로그램을 작성하면 됩니다. 소프트웨어를 작성하는 과정에서 프로그램의 특성에 따라 서버와 클라이언트 사이의 데이터 전송 규칙을 결정하는 것이 필요하며, 이것이 애플리케이션 계층 프로토콜이다. 네트워크 프로그래밍의 대부분은 애플리케이션 계층 프로토콜을 설계하고 구현하는 것입니다.

TCP 기반 서버 및 클라이언트 구현

TCP 서버 측의 기본 함수 호출 순서

1、socket()    		创建套接字
2、bind()			分配套接字地址
3、listen()			等待连接请求状态
4、accept()			允许连接
5、read()/write()	数据交换
6、close()			断开连接

대기 중인 연결 요청 상태로 들어갑니다.

소켓에 주소를 할당하기 위해 바인드 함수를 호출했고, 다음으로 청취 함수를 호출하여 연결 요청을 기다리는 상태로 진입했습니다. 청취 함수를 호출해야만 클라이언트가 연결을 발행할 수 있는 상태로 진입할 수 있습니다. request.이때 클라이언트는 connect.함수를 호출할 수 있습니다.

#include<sys/socket.h>

int listen(int sock, int backlog);
	成功返回0,失败返回-1
    sock	为希望进入等待连接请求状态的文件描述符,传递的描述符套接字参数为服务器端套接字
    backlog 连接请求等待队列的长度,若为5,则队列长度为5,表示最多使5个连接请求进入队列

연결 요청 대기 상태란 클라이언트가 연결을 요청하면 연결을 수락하기 전에 연결을 대기 상태로 유지하는 것을 의미합니다. 클라이언트 연결 요청 자체도 네트워크에서 수신하는 일종의 데이터이므로 수락하려면 소켓이 필요합니다. 그것.

클라이언트 연결 요청 수락

Listen 함수를 호출한 후 새로운 연결 요청이 있으면 순서대로 수락해야 합니다. 요청을 수락한다는 것은 데이터를 수락할 수 있는 상태로 들어가는 것을 의미하며, 이때 데이터를 수락하기 위해서는 소켓이 필요하지만, 서버측 소켓은 게이트키퍼 역할을 하고 있어 더 이상 데이터를 수락하는 역할을 할 수 없습니다. 따라서 또 다른 소켓이 필요하며 이 소켓은 개인적으로 생성할 필요가 없습니다. accept 함수는 소켓을 생성하고 요청을 시작한 클라이언트에 연결합니다.

#include <sys/socket.h>

int accept(int sock, struct sockaddr* addr, socklen_t* addrlen);
	成功返回创建的套接字文件描述符,失败返回-1
    sock  	服务器套接字的文件描述符
	addr  	保存发起连接请求的客户端地址信息的变量的地址,调用函数后会向该变量填充客户端地址信息
	addrlen	第二个参数addr结构体的长度,调用函数后会向该变量填充客户端地址长度

수락 기능은 연결 요청을 수락하고 대기열에서 보류 중인 클라이언트 연결 요청을 기다립니다. 함수 호출이 성공하면 accept 함수는 내부적으로 데이터 I/O를 위한 소켓을 생성하고 파일 설명자를 반환하며, 소켓이 자동으로 생성되고 연결 요청을 시작한 클라이언트와 자동으로 연결이 설정됩니다.

TCP 클라이언트의 기본 함수 호출 순서

TCP 클라이언트 함수 호출 순서

1、socket()			创建套接字
2、connect()			请求连接
3、read()/write()	交换数据
4、closr()			断开连接

서버 측과 비교하여 차이점은 연결 요청에 있는데, 이는 클라이언트 소켓을 생성한 후 서버 측에 연결 요청을 시작하게 하며, 서버 측에서는 Listen 함수를 호출하고 요청 대기 대기열을 생성한 후 클라이언트가 요청할 수 있습니다. 연결.

#include<sys/socket.h>

int connect(int sock, struct sockaddr* servaddr, socklen_t addrlen);
	成功返回0,失败返回-1
     sock  		客户端套接字文件描述符
	 servaddr	保存目标服务器地址信息的变量地址值
	 addrlen	以字节为单位,传递第二个参数的地址变量的长度

클라이언트가 연결 함수를 호출한 후 다음 상황이 발생하는 경우에만 반환됩니다.

  • 서버가 연결 요청을 수락합니다.
  • 네트워크 연결 끊김 등의 비정상적인 상황으로 인해 연결 요청이 중단되었습니다.

연결 요청을 수락한다고 해서 서버가 수락 함수를 호출하는 것은 아니며 실제로 서버는 연결 요청 정보를 대기 큐에 기록하므로 연결 함수가 반환된 직후에는 데이터 교환이 수행되지 않습니다.

TCP 기반의 서버 측과 클라이언트 측 함수 호출 관계

여기에 이미지 설명을 삽입하세요.

전반적인 과정은 다음과 같습니다.

서버는 소켓을 생성한 후 지속적으로 바인딩 및 수신 함수를 호출하여 대기 상태로 진입합니다. 클라이언트는 연결 함수를 호출하여 연결 요청을 시작합니다. 클라이언트는 연결을 시작하기 위해 연결을 호출하기 전에 서버가 수신 함수를 호출할 때까지 기다립니다. 클라이언트 역시 독립적이어야 하며, Connect 함수를 호출하기 전에 서버가 Accept 함수를 먼저 호출할 수도 있는데, 이때 서버는 Accept 함수를 호출하고 클라이언트가 Connect 함수를 호출할 때까지 Blocking 상태에 들어간다.

반복 서버 측 및 클라이언트 측 구현

반복 서버 측 구현

여기에 이미지 설명을 삽입하세요.

서버 측에서 반복을 구현하는 가장 간단한 방법은 루프 문을 삽입하고 accept 함수를 반복적으로 호출하는 것입니다. 루프 끝의 close(클라이언트)는 accept 함수를 호출하여 생성된 소켓을 닫는데, 이는 특정 클라이언트에 대한 서비스가 종료되었음을 의미하며, 이때에도 여전히 다른 클라이언트에 서비스를 제공하려면 accept 함수를 호출해야 합니다. 다시 기능합니다. 현재는 동시에 하나의 클라이언트에만 서비스를 제공할 수 있지만 프로세스와 스레드를 학습한 후에는 동시에 여러 클라이언트에 서비스를 제공하는 서버를 작성할 수 있습니다.

반복 에코 서버 측, 클라이언트 측

에코 서버 및 지원하는 에코 클라이언트의 기본 작동 모드:

  • 서버는 동시에 하나의 클라이언트에만 연결되며 에코 서비스를 제공합니다.
  • 서버는 5개의 클라이언트에게 차례로 서비스를 제공하고 종료됩니다.
  • 클라이언트는 사용자의 입력 문자열을 수락하고 이를 서버로 보냅니다.
  • 서버는 수신된 문자열 데이터를 다시 클라이언트, 즉 "echo"로 전송합니다.
  • 두 끝 사이의 문자열 echo는 클라이언트가 Q를 입력할 때까지 수행됩니다.

먼저 위의 요구사항을 충족하는 에코 서버를 소개합니다.

echo_server.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 1024
void error_handling(char *message);

int main(int argc, char *argv[])
{
	int serv_sock, clnt_sock;
	char message[BUF_SIZE];
	int str_len, i;
	
	struct sockaddr_in serv_adr;
	struct sockaddr_in clnt_adr;
	socklen_t clnt_adr_sz;
	
	if(argc!=2) {
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}
	
	serv_sock=socket(PF_INET, SOCK_STREAM, 0);   
	if(serv_sock==-1)
		error_handling("socket() error");
	
	memset(&serv_adr, 0, sizeof(serv_adr));
	serv_adr.sin_family=AF_INET;
	serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
	serv_adr.sin_port=htons(atoi(argv[1]));

	if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
		error_handling("bind() error");
	
	if(listen(serv_sock, 5)==-1)
		error_handling("listen() error");
	
	clnt_adr_sz=sizeof(clnt_adr);

	for(i=0; i<5; i++)
	{
		clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
		if(clnt_sock==-1)
			error_handling("accept() error");
		else
			printf("Connected client %d \n", i+1);
	
		while((str_len=read(clnt_sock, message, BUF_SIZE))!=0)
			write(clnt_sock, message, str_len);

		close(clnt_sock);
	}

	close(serv_sock);
	return 0;
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

작업 결과

gcc echo_server.c -o eserver
./eserver 9190
输出:
Connecten client 1
Connecten client 2
Connecten client 3

에코 클라이언트 코드

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 1024
void error_handling(char *message);

int main(int argc, char *argv[])
{
	int sock;
	char message[BUF_SIZE];
	int str_len;
	struct sockaddr_in serv_adr;

	if(argc!=3) {
		printf("Usage : %s <IP> <port>\n", argv[0]);
		exit(1);
	}
	
	sock=socket(PF_INET, SOCK_STREAM, 0);   
	if(sock==-1)
		error_handling("socket() error");
	
	memset(&serv_adr, 0, sizeof(serv_adr));
	serv_adr.sin_family=AF_INET;
	serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
	serv_adr.sin_port=htons(atoi(argv[2]));
	
	if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
		error_handling("connect() error!");
	else
		puts("Connected...........");
	
	while(1) 
	{
		fputs("Input message(Q to quit): ", stdout); 	//如果输入Q说明结束while循环
		fgets(message, BUF_SIZE, stdin);
		
		if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))  //检验message是否为Q/q
			break;

		write(sock, message, strlen(message));
		str_len=read(sock, message, BUF_SIZE-1);
		message[str_len]=0;
		printf("Message from server: %s", message);
	}
	
	close(sock);
	return 0;
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

작업 결과

gcc echo_client.c -o eclient
./eclient 192.168.233.20 9190
输出:
Connected ....
Input message: hello
Message from server: hello
Input message : Q

이 글은 "TCP/IP 네트워크 프로그래밍" 칼럼의 네 번째 글입니다. 독자 여러분의 구독을 환영합니다!

자세한 내용을 보려면 GitHub 를 클릭하세요 . Star에 오신 것을 환영합니다.

⭐학술교류그룹 Q 754410389 업데이트중입니다~~

추천

출처blog.csdn.net/m0_63743577/article/details/132707211