1. 네트워크 내 프로세스 간 통신 방법
프로세스 통신의 개념은 원래 독립형 시스템에서 나왔습니다. 각 프로세스는 자신의 주소 범위 내에서 실행되므로 두 통신 프로세스가 서로 간섭하지 않고 조화롭게 작동하도록 운영 체제에서는
UNIX와 같은 프로세스 통신을 위한 해당 기능을 제공합니다. BSD에는 파이프(pipe), 명명된 파이프(namedpipe) 소프트 인터럽트 신호(signal)
UNIX 시스템 V에는 메시지(message), 공유 저장 영역(shared memory), 세마포 등이 있습니다.
로컬 프로세스 간의 통신으로 제한됩니다. 인터넷 프로세스 통신은 서로 다른 호스트 프로세스 간의 상호 통신 문제를 해결하는 것을 목표로 합니다(동일 머신에서의 프로세스 통신은 특별한 경우로 간주할 수 있음). 이를 위해 가장 먼저 해결해야 할 것은 네트워크 간 프로세스 식별 문제이다. 동일한 호스트에서 서로 다른 프로세스는 프로세스 ID로 고유하게 식별될 수 있습니다. 그러나 네트워크 환경에서는 각 호스트가 독립적으로 할당한 프로세스 번호로는 프로세스를 고유하게 식별할 수 없습니다. 예를 들어, 호스트 A는 프로세스 번호 5를 할당했는데, 프로세스 번호 5는 호스트 B에도 존재할 수 있습니다. 따라서 "프로세스 번호 5"라는 문장은 의미가 없습니다. 둘째, 운영 체제는 많은 네트워크 프로토콜을 지원하며, 서로 다른 프로토콜은 서로 다른 방식으로 작동하고 서로 다른 주소 형식을 갖습니다. 따라서 네트워크 간 프로세스 통신은 여러 프로토콜을 식별하는 문제도 해결해야 합니다.
실제로 TCP/IP 프로토콜 제품군은 이 문제를 해결하는 데 도움이 되었습니다. 네트워크 계층의 "IP 주소"는 네트워크의 호스트를 고유하게 식별할 수 있는 반면 전송의 "프로토콜 + 포트"는 계층은 호스트 애플리케이션(프로세스)을 고유하게 식별할 수 있습니다. 이러한 방식으로 삼중항(IP 주소, 프로토콜, 포트)을 사용하여 네트워크 프로세스를 식별할 수 있으며, 네트워크의 프로세스 통신은 이 표시를 사용하여 다른 프로세스와 상호 작용할 수 있습니다.
TCP/IP 프로토콜을 사용하는 애플리케이션은 일반적으로 네트워크 프로세스 간의 통신을 달성하기 위해 UNIX BSD의 소켓과 UNIX System V의 TLI(이미 사용되지 않음)와 같은 애플리케이션 프로그래밍 인터페이스를 사용합니다. 현재는 거의 모든 응용 프로그램이 소켓을 사용하고 있으며 이제는 네트워크상의 프로세스 통신이 어디에나 존재합니다. 이것이 제가 "모든 것이 소켓입니다"라고 말하는 이유입니다.
2. TCP/IP, UDP란
TCP/ IP(전송 제어 프로토콜/인터넷 프로토콜)는 광역 네트워크(WAN)용으로 설계된 산업 표준 프로토콜 세트입니다.
OS에는 TCP/IP 프로토콜이 존재하며, OS를 통해 네트워크 서비스가 제공되는데, TCP/IP를 지원하는 시스템 호출이 OS에 추가됩니다. Socket, Connect, Send, Recv 등의 Berkeley 소켓이 추가됩니다.
UDP(User Data Protocol)는 TCP에 해당하는 프로토콜입니다. 이는 TCP/IP 프로토콜 제품군의 구성원입니다. 그림과 같습니다:
TCP/IP 프로토콜 제품군에는 전송 계층, 네트워크 계층 및 링크 계층이 포함됩니다. 소켓의 위치는 그림과 같습니다. 소켓은 애플리케이션 계층이자 제품군 통신을 위한 중간 소프트웨어 추상화 계층입니다.
3. 소켓이란
1. 소켓:
Unix에서 유래했으며 Unix/Linux의 기본 철학 중 하나는 "모든 것이 파일이다"라는 것이며 "열기 -> 읽기 및 쓰기 쓰기/읽기 -> 닫기" 모드에서 작동할 수 있습니다. 소켓은 이 모드의 구현입니다. 소켓은 특수 파일이며 일부 소켓 기능은 이에 대한 작업(읽기/쓰기 IO, 열기, 닫기)입니다.
직설적으로 말하면 소켓은 애플리케이션 계층이며 TCP/IP입니다. 인터페이스 집합인 프로토콜 계열 통신을 위한 중간 소프트웨어 추상화 계층입니다. 디자인 모드에서 소켓은 실제로 소켓 인터페이스 뒤에 복잡한 TCP/IP 프로토콜 제품군을 숨기는 파사드 모드입니다. 사용자에게는 소켓이 지정된 요구 사항을 충족하도록 데이터를 구성할 수 있는 간단한 인터페이스 집합이 있습니다.
참고: 실제로 소켓에는 레이어 개념이 없습니다. 이는 단지 외관 디자인 패턴을 적용한 것일 뿐이므로 프로그래밍이 더 쉽습니다. 소프트웨어 추상화 계층입니다. 네트워크 프로그래밍에서는 많은 소켓을 사용합니다.
2. 소켓 설명자
실제로는 정수입니다. 우리에게 가장 친숙한 세 개의 핸들은 0, 1, 2입니다. 0은 표준 입력, 1은 표준 출력, 2는 표준 오류 출력입니다. 0, 1, 2는 정수로 표시되고 해당 FILE * 구조는 stdin, stdout, stderr로 표시됩니다.
소켓 API는 원래 UNIX 운영 체제의 일부였습니다. system 소켓 API가 시스템의 다른 I/O 장치와 통합되도록 개발되었습니다. 특히, 애플리케이션이 인터넷 통신을 위한 소켓을 생성할 때 운영 체제는 소켓을 식별하기 위한 설명자로 작은 정수를 반환합니다. 그런 다음 애플리케이션은 설명자를 매개변수로 전달하고 함수를 호출하여 일부 작업(예: 네트워크를 통해 데이터 전송 또는 수신 데이터 수신)을 완료합니다.
많은 운영 체제에서는 소켓 설명자와 기타 I/O 설명자가 통합되어 있으므로 애플리케이션은 소켓 I/O 또는 파일에 대한 I/O 읽기/쓰기 작업을 수행할 수 있습니다.
애플리케이션이 소켓을 생성하려고 하면 운영체제는 작은 정수를 디스크립터로 반환하고, 애플리케이션은 이 디스크립터를 사용하여 소켓에 대한 I/O 요청이 필요한 애플리케이션 요청을 참조합니다. 시스템이 파일을 엽니다. 운영 체제는 응용 프로그램이 파일에 액세스할 수 있도록 파일 설명자를 만듭니다. 애플리케이션의 관점에서 파일 설명자는 애플리케이션이 파일을 읽고 쓰는 데 사용할 수 있는 정수입니다. 아래 그림은 운영 체제가 내부 데이터 구조를 가리키는 포인터 배열로 파일 설명자를 구현하는 방법을 보여줍니다.
프로그램 체계별로 별도의 표가 있습니다. 정확하게 말하면 시스템은 실행 중인 각 프로세스에 대해 별도의 파일 설명자 테이블을 유지 관리합니다. 프로세스가 파일을 열면 시스템은 파일의 내부 데이터 구조에 대한 포인터를 파일 설명자 테이블에 쓰고 테이블의 인덱스 값을 호출자에게 반환합니다. 애플리케이션은 이 설명자를 기억하고 나중에 파일을 조작할 때 이를 사용하기만 하면 됩니다. 운영 체제는 이 설명자를 인덱스로 사용하여 프로세스 설명자 테이블에 액세스하고 포인터를 사용하여 파일에 대한 모든 정보를 보유하는 데이터 구조를 찾습니다.
소켓의 시스템 데이터 구조:
1) 소켓 API에는 소켓을 생성하는 데 사용되는 소켓 함수가 있습니다. 소켓 설계의 일반적인 아이디어는 소켓이 매우 일반적이기 때문에 단일 시스템 호출로 모든 소켓을 생성할 수 있다는 것입니다. 소켓이 생성되면 애플리케이션은 특정 세부 정보를 지정하기 위해 다른 함수를 호출해야 합니다. 예를 들어, 소켓을 호출하면 새로운 설명자 항목이 생성됩니다:
2) 소켓의 내부 데이터 구조에는 많은 필드가 포함되어 있지만 시스템이 소켓을 생성한 후에는 대부분 단어 필드는 채워지지 않은 상태로 남아 있습니다. 애플리케이션이 소켓을 생성한 후 소켓을 사용하려면 먼저 다른 프로시저를 호출하여 이러한 필드를 채워야 합니다.
3. 파일 설명자와 파일 포인터의 차이점:
파일 설명자: Linux 시스템에서 파일을 열면 파일 설명자를 얻게 됩니다. 작은 양의 정수입니다. 각 프로세스는 PCB(Process Control Block)에 파일 설명자 테이블을 저장합니다. 파일 설명자는 이 테이블의 인덱스입니다.
파일 포인터: 파일 포인터는 C 언어에서 I/O 핸들로 사용됩니다. 파일 포인터는 프로세스의 사용자 영역에 있는 FILE 구조라는 데이터 구조를 가리킵니다. FILE 구조에는 버퍼와 파일 설명자가 포함됩니다. 파일 설명자는 파일 설명자 테이블에 대한 인덱스이므로 어떤 의미에서 파일 포인터는 핸들의 핸들입니다(Windows 시스템에서는 파일 설명자를 파일 핸들이라고 함).
4. 기본 SOCKET 인터페이스 기능
살면서 A가 B에게 전화를 걸고 싶은데, B가 전화벨 소리를 듣게 된다. 전화를 언급하면 A와 B는 연결되고 A와 B는 대화할 수 있습니다. 통화가 끝나면 전화를 끊어 통화를 종료하세요. 통화에서는 이것이 "열기-쓰기/읽기-닫기" 모드라는 간단한 방식으로 작동하는 방식을 설명했습니다.
서버는 먼저 소켓을 초기화한 다음 포트에 바인드하고 포트를 수신 대기하며 차단을 위해 Accept를 호출하고 클라이언트가 연결될 때까지 기다립니다. 이때 클라이언트가 Socket을 초기화한 후 서버에 연결(connect)하면, 연결에 성공하면 클라이언트와 서버 간의 연결이 성립된다. 클라이언트는 데이터 요청을 보내고, 서버는 요청을 받아 처리한 후, 응답 데이터를 클라이언트에 보내고, 클라이언트는 데이터를 읽고, 마지막으로 연결을 닫고 상호작용이 종료됩니다.
이러한 인터페이스의 구현은 커널에 의해 완료됩니다. 구현 방법에 대한 자세한 내용은 Linux 커널
4.1, 소켓() 함수
int 소켓(int protofamily, int type)을 참조하세요. , int 프로토콜); //sockfd를 반환합니다
sockfd는 설명자입니다.
소켓 함수는 일반 파일을 여는 작업에 해당합니다. 일반적인 파일 열기 작업은 파일 설명자를 반환하고, 소켓()을 사용하여 소켓을 고유하게 식별하는 소켓 설명자(소켓 설명자)를 생성합니다. 이 소켓 설명자는 파일 설명자와 동일하며 일부 읽기 및 쓰기 작업을 수행하기 위한 매개변수로 사용됩니다.
fopen에 다양한 매개변수 값을 전달하여 다양한 파일을 열 수 있는 것과 같습니다. 소켓을 생성할 때 다양한 매개변수를 지정하여 다양한 소켓 설명자를 생성할 수도 있습니다. 소켓 함수의 세 가지 매개변수는 다음과 같습니다.
protofamily: 즉, 프로토콜 제품군이라고도 하는 프로토콜 도메인입니다. 일반적으로 사용되는 프로토콜 제품군에는 AF_INET(IPV4), AF_INET6(IPV6), AF_LOCAL(또는 AF_UNIX, Unix 도메인 소켓), AF_ROUTE 등이 포함됩니다. 프로토콜 패밀리는 소켓의 주소 유형을 결정하며 해당 주소를 통신에 사용해야 합니다. 예를 들어 AF_INET은 ipv4 주소(32비트)와 포트 번호(16비트)의 조합을 사용하도록 결정하고 AF_UNIX는 결정합니다. 절대 경로를 주소로 사용합니다.
유형: 소켓 유형을 지정합니다. 일반적으로 사용되는 소켓 유형에는 SOCK_STREAM, SOCK_DGRAM, SOCK_RAW, SOCK_PACKET, SOCK_SEQPACKET 등이 있습니다. (소켓 유형은 무엇입니까?)
프로토콜: 따라서 이름이 지정된 프로토콜을 의미합니다. 일반적으로 사용되는 프로토콜에는 IPPROTO_TCP, IPPTOTO_UDP, IPPROTO_SCTP, IPPROTO_TIPC 등이 있으며 각각 TCP 전송 프로토콜, UDP 전송 프로토콜, STCP 전송 프로토콜 및 TIPC 전송 프로토콜에 해당합니다(이 프로토콜에 대해서는 별도로 설명하겠습니다!).
참고: 위의 유형과 프로토콜은 임의로 결합할 수 없습니다. 예를 들어 SOCK_STREAM은 IPPROTO_UDP와 결합할 수 없습니다. 프로토콜이 0이면 유형 유형에 해당하는 기본 프로토콜이 자동으로 선택됩니다.
소켓을 생성하기 위해 소켓을 호출할 때 반환된 소켓 설명자는 프로토콜 패밀리(주소 패밀리, AF_XXX) 공간에 존재하지만 특정 주소를 갖지 않습니다. 주소를 할당하려면 반드시 바인딩() 함수를 호출해야 합니다. 그렇지 않으면 시스템은 connect() 또는 Listen()을 호출할 때 자동으로 포트를 무작위로 할당합니다.
4.2.bind() 함수
위에서 언급한 것처럼 바인딩() 함수는 주소 계열의 특정 주소를 소켓에 할당합니다. 예를 들어, AF_INET 및 AF_INET6에 대응하여 ipv4 또는 ipv6 주소와 포트 번호 조합이 소켓에 할당됩니다.
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
함수의 세 가지 매개변수는 다음과 같습니다.
sockfd: 즉, 소켓() 함수를 통해 생성되고 소켓을 고유하게 식별하는 소켓 설명 Word입니다. 바인딩() 함수는 이 설명자에 이름을 바인딩합니다.
addr: sockfd에 바인딩될 프로토콜 주소를 가리키는 const struct sockaddr * 포인터. 이 주소 구조는 주소가 소켓을 생성할 때 주소 프로토콜 계열에 따라 다릅니다. 예를 들어 ipv4는 다음과 같습니다.
struct sockaddr_in {
sa_family_t sin_family; /* 주소 계열: AF_INET */
in_port_t sin_port; /* 네트워크 바이트 순서의 포트 */
struct in_addr sin_addr; /* 인터넷 주소 */
};
/* 인터넷 주소. in_addr {
uint32_t s_addr; /* 네트워크 바이트 순서의 주소 */
};
sa_family_t sin6_family; /* AF_INET6 */
in_port_t sin6_port; /* 포트 번호 */
uint32_t sin6_flowinfo; /* IPv6 흐름 정보 */
struct in6_addr sin6_addr ; sin6_scope_id; /* 범위 ID(2.4의 새로운 기능) */
};
struct in6_addr {
unsigned char s6_addr[16] /* IPv6 주소 */
};
다음에 해당하는 Unix 도메인:
#define UNIX_PATH_MAX 108
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */
char path sun_path[UNIX_PATH_MAX] /* 이름 */
};
addrlen: 주소의 길이에 해당합니다.
일반적으로 서버가 시작되면 잘 알려진 주소(예: IP 주소 + 포트 번호)를 바인딩하여 서비스를 제공하며 클라이언트는 이를 통해 서버에 연결할 수 있습니다. 지정하면 시스템이 자동으로 포트 번호와 자체 IP 주소 조합을 할당합니다. 이것이 서버가 일반적으로 수신 대기 전에 바인딩()을 호출하지만 클라이언트는 이를 호출하지 않는 대신 시스템이 connect() 중에 임의로 생성하는 이유입니다.
네트워크 바이트 순서 및 호스트 바이트 순서
호스트 바이트 순서는 우리가 일반적으로 빅 엔디안 및 리틀 엔디안 모드라고 부르는 것입니다. CPU마다 바이트 순서 유형이 다르며, 이 단어는 섹션 순서를 나타냅니다. 정수가 메모리에 저장되는 것을 호스트 순서라고 합니다. Big-Endian과 Little-Endian의 표준 정의는 다음과 같습니다.
a) Little-Endian은 하위 바이트가 메모리의 하위 주소 끝에 배열되고 상위 바이트가 배열된다는 의미입니다. 바이트는 메모리의 상위 주소 끝에 배열됩니다.
b) Big-Endian은 상위 바이트가 메모리의 하위 주소 끝에 배열되고, 하위 바이트가 메모리의 상위 주소 끝에 배열되는 것을 의미합니다.
네트워크 바이트 순서: 4바이트 32비트 값은 먼저 0~7비트, 그 다음 8~15비트, 16~23비트, 마지막으로 24~31비트의 순서로 전송됩니다. 이 전송 순서를 빅엔디안이라고 합니다. TCP/IP 헤더의 모든 이진 정수는 네트워크를 통해 전송될 때 이 순서로 되어 있어야 하므로 이를 네트워크 바이트 순서라고도 합니다. 바이트 순서는 이름에서 알 수 있듯이 1바이트 유형보다 큰 데이터가 메모리에 저장되는 순서입니다. 1바이트 데이터에는 순서 문제가 없습니다.
따라서: 주소를 소켓에 바인딩할 때 먼저 호스트 바이트 순서를 네트워크 바이트 순서로 변환하고 호스트 바이트 순서가 네트워크 바이트 순서와 동일한 Big을 사용한다고 가정하지 마십시오. -엔디안. 이 문제로 인해 살인 사건이 발생했습니다! 이 문제로 인해 회사의 프로젝트 코드에 설명할 수 없는 많은 문제가 발생하였으므로 호스트 바이트 순서에 대해 어떤 가정도 하지 마시고, 소켓에 할당하기 전에 반드시 네트워크 바이트 순서로 변환하시기 바랍니다.
4.3, Listen(), connect() 함수
서버인 경우, 소켓()과 바인딩()을 호출한 후, Listen()이 호출되어 소켓을 수신합니다. 클라이언트는 그런 다음 connect()를 호출하여 연결 요청을 발행하고 서버는 요청을 수신합니다.
int listening(int sockfd, int backlog);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
listen의 첫 번째 항목 function 매개변수는 모니터링할 소켓 설명자이고, 두 번째 매개변수는 해당 소켓에 대해 대기할 수 있는 최대 연결 수입니다. 소켓() 함수에 의해 생성된 소켓은 기본적으로 능동형이며, Listen 함수는 소켓을 수동형으로 변경하여 클라이언트의 연결 요청을 기다린다.
connect 함수의 첫 번째 매개변수는 클라이언트의 소켓 설명자이고, 두 번째 매개변수는 서버의 소켓 주소, 세 번째 매개변수는 소켓 주소의 길이입니다. 클라이언트는 connect 함수를 호출하여 TCP 서버와 연결을 설정합니다.
4.4.accept() 함수
TCP 서버는 소켓(), 바인드(), 청취()를 차례로 호출한 후 지정된 소켓 주소를 수신합니다. TCP 클라이언트는 Socket()과 connect()를 차례로 호출한 후 TCP 서버에 연결 요청을 보냅니다. TCP 서버는 이 요청을 수신한 후 accept() 함수를 호출하여 요청을 수신하고 연결이 설정됩니다. 그런 다음 일반 파일 읽기 및 쓰기 I/O 작업과 유사한 네트워크 I/O 작업을 시작할 수 있습니다.
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //연결 반환 connect_fd
매개변수 sockfd
매개변수 sockfd는 위에서 설명한 청취 소켓입니다. 이 소켓은 클라이언트가 서버에 연결할 때 이 포트 번호를 사용하며 이 포트 번호는 소켓 연결과 정확히 동일합니다. 물론 클라이언트는 소켓의 세부 사항을 알지 못하며 주소와 포트 번호만 알고 있습니다.
Parameter addr
반환 값을 받아들이는 데 사용되는 결과 매개 변수입니다. 이 반환 값은 클라이언트의 주소를 지정합니다. 물론 이 주소는 주소 구조를 통해 설명됩니다. . 사용자 이것이 어떤 종류의 주소 구조인지 알아야 합니다. 고객의 주소에 관심이 없는 경우 이 값을 NULL로 설정할 수 있습니다.
매개변수 len
모두가 생각하는 것처럼 결과의 매개변수이기도 합니다. 이는 위 addr 구조의 크기를 받아들이는 데 사용됩니다. addr이 차지하는 바이트 수를 지정합니다. 구조. 마찬가지로 NULL로 설정할 수도 있습니다.
accept가 성공적으로 반환되면 서버와 클라이언트가 올바르게 연결된 것입니다. 이때 서버는 accept가 반환한 소켓을 통해 클라이언트와의 통신을 완료합니다.
참고:
기본적으로 accept는 클라이언트 연결이 설정되고 반환될 때까지 프로세스를 차단합니다. 이는 연결 소켓입니다.
이 시점에서 두 가지 유형의 소켓을 구별해야 합니다.
청취 소켓: 청취 소켓은 accept의 sockfd 매개변수와 같습니다. 청취 함수를 호출한 후, 소켓() 함수 호출을 시작하는 서버에 의해 생성됩니다. 소켓 설명자(수신 소켓)
연결 소켓: 소켓은 활성 연결된 소켓에서 수신 소켓으로 변환되고 수락 함수는 기존 지점을 나타내는 연결된 소켓 설명자(연결 소켓)를 반환합니다. 네트워크의 대지점 연결.
서버는 일반적으로 서버 수명 주기 동안 항상 존재하는 청취 소켓 설명자만 생성합니다. 커널은 서버 프로세스가 수락한 각 클라이언트 연결에 대해 연결된 소켓 설명자를 생성합니다. 서버가 클라이언트 서비스를 완료하면 해당 연결된 소켓 설명자가 닫힙니다.
자연스러운 질문은 다음과 같습니다. 왜 두 가지 유형의 소켓이 있습니까? 그 이유는 간단합니다. 설명자를 사용하면 기능이 너무 많아 사용이 매우 직관적이지 않게 됩니다. 동시에 이러한 새로운 설명자는 실제로 커널에서 생성됩니다.
연결 소켓 소켓fd_new는 클라이언트와 통신하기 위해 새 포트를 차지하지 않습니다. 여전히 수신 소켓 소켓fd와 동일한 포트 번호를 사용합니다.
4.5, read() , write () 및 기타 기능
모든 것이 준비되었지만 동풍이 필요한 시점에서 서버와 클라이언트 간의 연결이 설정되었습니다. 읽기 및 쓰기 작업을 위해 네트워크 I/O를 호출할 수 있으며, 이는 네트워크의 서로 다른 프로세스 간의 통신이 실현된다는 것을 의미합니다! 네트워크 I/O 작업에는 다음 그룹이 있습니다.
read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()
recvmsg()/sendmsg() 함수를 사용하는 것이 좋습니다. 이 두 함수가 가장 좋습니다. 다양한 I/O 기능을 사용하면 실제로 위의 다른 기능을 이 두 기능으로 대체할 수 있습니다. 해당 선언은 다음과 같습니다:
#include
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
#include
#include
ssize_t send(int sockfd, const void *buf, size_t len, int 플래그);
ssize_t recv(int sockfd, void *buf, size_t len, int 플래그);
ssize_t sendto(int sockfd, const void *buf, size_t len , int 플래그,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
읽기 함수 읽기가 성공하면 read는 읽은 실제 바이트 수를 반환합니다. 반환된 값이 0이면 파일의 끝이 0보다 작다는 의미입니다. 오류가 발생했다는 의미입니다. 오류가 EINTR이면 인터럽트로 인해 읽기가 발생했음을 의미합니다. ECONNREST이면 네트워크 연결에 문제가 있음을 의미합니다.
write 함수는 buf의 nbytes 바이트 내용을 파일 설명자 fd에 씁니다. 성공 시 작성된 바이트 수를 반환합니다. 실패하면 -1이 반환되고 errno 변수가 설정됩니다. 네트워크 프로그램에서 소켓 파일 설명자에 쓸 때 두 가지 가능성이 있습니다. 1) 쓰기의 반환 값이 0보다 크면 데이터의 일부 또는 전체가 기록되었음을 나타냅니다. 2) 반환된 값이 0보다 작아서 오류가 발생했습니다. 오류 유형에 따라 처리해야 합니다. 오류가 EINTR이면 쓰기 중에 인터럽트 오류가 발생했음을 의미합니다. EPIPE인 경우 네트워크 연결에 문제가 있음을 의미합니다(상대방이 연결을 끊었습니다).
이러한 I/O 기능 쌍을 하나씩 소개하지는 않겠습니다. 자세한 내용은 맨 문서를 참조하거나 다음 예에서는 Send/recv를 사용합니다.
4.6.close() 함수
서버가 클라이언트와 연결을 설정한 후 읽기 및 쓰기 작업이 완료된 후 해당 소켓 설명자가 수행되어야 합니다. 파일을 연 후 fclose를 호출하여 열린 파일을 닫습니다.
#include
int close(int fd);
close의 기본 동작은 TCP 소켓을 닫을 때 소켓을 닫힌 것으로 표시하는 것입니다. .그런 다음 즉시 호출 프로세스로 돌아갑니다. 이 설명자는 호출 프로세스에서 더 이상 사용할 수 없습니다. 즉, 더 이상 읽기 또는 쓰기의 첫 번째 매개변수로 사용할 수 없습니다.
참고: 닫기 작업은 해당 소켓 설명자의 참조 개수를 -1만큼만 줄입니다. 참조 개수가 0인 경우에만 TCP 클라이언트가 트리거되어 서버에 종료 요청을 보냅니다.
5. 소켓에 TCP 구축(3방향 핸드셰이크)
TCP 프로토콜은 세 개의 메시지 세그먼트를 통해 연결 설정을 완료합니다. 이 프로세스를 3방향 핸드셰이크라고 합니다.
첫 번째 핸드셰이크: 연결을 설정할 때 클라이언트는 서버에 syn 패킷(syn=j)을 보내고 SYN_SEND 상태로 들어가 서버의 확인을 기다립니다. SYN: 시퀀스 번호 동기화.
두 번째 핸드셰이크: 서버는 syn 패킷을 수신하고 클라이언트의 SYN(ack=j+1)을 확인하는 동시에 SYN 패킷(syn=k)도 보냅니다. 는 SYN+ACK 패키지입니다. 이때 서버는 SYN_RECV 상태에 들어갑니다.
세 번째 핸드셰이크: 클라이언트는 서버로부터 SYN+ACK 패키지를 수신하고 확인 패키지 ACK(ack=k+1)를 클라이언트로 보냅니다. 패키지가 전송된 후 클라이언트는 서버와 함께 ESTABLISHED 상태에 들어가고 3방향 핸드셰이크를 완료합니다.
완전한 3방향 핸드셰이크는 요청-응답-다시 확인입니다.
해당 함수 인터페이스 :
그림에서 볼 수 있듯이 클라이언트가 connect를 호출하면 연결 요청이 트리거되고 SYN이 전송됩니다. 그런 다음 연결이 차단 상태에 들어가고 서버는 연결 요청을 모니터링하고 SYN J 패키지를 수신하고 요청을 수신하기 위해 accept 함수를 호출한 후 클라이언트에 SYN K, ACK J+1을 보냅니다. 클라이언트는 서버로부터 SYN K와 ACK J+1을 수신한 후 SYN K를 반환하고 서버가 ACK K+1을 수신하면 수락을 반환합니다. 핸드셰이크가 완료되고 연결이 설정되는 방식입니다.
네트워크 패킷 캡처를 통해 특정 프로세스를 볼 수 있습니다.
예를 들어 우리 서버는 포트 9502를 엽니다. tcpdump를 사용하여 패킷을 캡처합니다.
tcpdump -iany tcp port 9502
그런 다음 telnet 127.0을 사용합니다. .0.1 9502 연결 열기.:
telnet 127.0.0.1 9502
14:12:45.104687 IP localhost.39870 > localhost.9502: 플래그 [S], seq 2927179378, win 32792, 옵션 [mss 16396, sackOK, TS val 255474104 ecr 0, nop, wscale 3], 길이 0 (1)
14:12:45.104701 IP localhost.9502 > localhost.39870: 플래그 [S.] , seq 1721825043, ack 2927179379, win 32768, 옵션 [mss 16396,sackOK,TS val 255474104 ecr 255474104,nop,wscale 3], 길이 0 (2)
14:12:45.104711 IP localhost.39 0 > .9502: 플래그 [.], ack 1, win 4099, 옵션 [nop,nop,TS val 255474104 ecr 255474104], 길이 0 (3)
14:13:01.415407 IP localhost.39870 > ; localhost.9502: 플래그 [P.], seq 1:8, ack 1, win 4099, 옵션 [nop,nop,TS val 255478182 ecr 255474104], 길이 7
14:13:01.415432 IP localhost.9502 > ; localhost.39870: 플래그 [.], ack 8, win 4096, 옵션 [nop,nop,TS val 255478182 ecr 255478182], 길이 0
14:13:01.415747 IP localhost.9502 > 플래그 [P.], seq 1:19, ack 8, win 4096, 옵션 [nop,nop,TS val 255478182 ecr 255478182], 길이 18
14:13:01.415757 IP localhost.39870 > 플래그 [.], ack 19, win 4097, 옵션 [nop,nop,TS val 255478182 ecr 255478182], 길이 0
114 :12:45.104687 시간은 미묘하게 정확
localhost.39870 > localhost.9502는 통신의 흐름을 나타내고, 39870은 클라이언트, 9502는 서버
[S] SYN 요청임을 나타냅니다.
[S.]는 SYN+ACK 확인 패킷임을 의미합니다.
[.]은 ACT 확인 패킷임을 의미합니다. (클라이언트)SYN ->(서버)SYN->(클라이언트)ACT는 3방향 핸드셰이크 프로세스입니다.
[P]는 이것이 서버에서 클라이언트로 또는 클라이언트에서 이루어질 수 있는 데이터 푸시임을 의미합니다.
[F]를 누르면 연결 종료 작업인 FIN 패킷이 표시됩니다. 클라이언트/서버는 이것이 RST 패킷임을 나타내는
[R]을 시작할 수 있습니다. , 이는 F 패킷과 동일한 효과가 있지만 RST는 연결이 닫혔을 때 여전히 처리되지 않은 데이터가 있음을 의미합니다. 강제로 연결을 끊는다고 이해하시면 됩니다
win 4099는 슬라이딩 윈도우 크기를 의미합니다
길이 18은 데이터 패킷의 크기를 의미합니다
보자 (1) (2) (3)의 세 단계는 tcp를 설정하는 것입니다:
첫 번째 핸드셰이크:
14:12:45.104687 IP localhost.39870 > ; localhost.9502: Flags [ S], seq 2927179378
클라이언트 IP localhost.39870 (클라이언트 포트는 일반적으로 자동으로 할당됨) syn 패키지(syn=j)를 서버 localhost.9502 서버로 보내기》
syn 패키지(syn=j): syn's seq= 2927179378(j=2927179378)
두 번째 핸드셰이크:
14:12:45.104701 IP localhost .9502 > ; localhost.39870: 플래그 [S.], seq 1721825043, ack 2927179379,
요청 수신 및 확인: 서버가 syn 패킷을 수신하고 클라이언트의 SYN(ack=j+1)을 확인해야 합니다. , 그리고 동시에 SYN 패킷(syn=k), 즉 SYN+ACK 패킷도 보냅니다:
이때 서버 호스트 자신의 SYN: seq: y= syn seq 1721825043.
ACK는 j+1 = (ack=j+1) =ack 2927179379
세 번째 핸드셰이크:
14:12:45.104711 IP localhost .39870 > localhost.9502: 플래그 [.], ack 1,
클라이언트는 서버로부터 SYN+ACK 패킷을 수신하고 서버에 승인 패킷 ACK를 보냅니다(ack=k+ 1 )
클라이언트와 서버가 ESTABLISHED 상태가 된 후 통신 데이터를 주고받을 수 있습니다. 이번에는 Accept 인터페이스와 관련이 없습니다. Accept가 없더라도 3방향 핸드셰이크가 완료됩니다.
连接出现连接不上的问题,一般是网路出现问题或者网卡超负荷或者是连接数已经满啦。
紫色背景的部分:
IP localhost.39870 > localhost.9502: Flags [P.], seq 1:8, ack 1, win 4099, options [nop,nop,TS val 255478182 ecr 255474104], length 7
客户端向服务器发送长度为7个字节的数据,
IP localhost.9502 > localhost.39870: Flags [.], ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 0
服务器向客户确认已经收到数据
IP localhost.9502 > localhost.39870: Flags [P.], seq 1:19, ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 18
然后服务器同时向客户端写入数据。
IP localhost.39870 > localhost.9502: Flags [.], ack 19, win 4097, options [nop,nop,TS val 255478182 ecr 255478182], length 0
客户端向服务器确认已经收到数据
这个就是tcp可靠的连接,每次通信都需要对方来确认。
6. Linux에서의 SOCKET 프로그래밍에 대한 자세한 설명
建立一个连接需要三次握手,而终止一个连接要经过四次握手,这是由TCP的半关闭(half-close)造成的,如图:
由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这个原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
(1)客户端A发送一个FIN,用来关闭客户A到服务器B的数据传送(报文段4)。
(2)服务器B收到这个FIN,它发回一个ACK,确认序号为收到的序号加1(报文段5)。和SYN一样,一个FIN将占用一个序号。
(3)服务器B关闭与客户端A的连接,发送一个FIN给客户端A(报文段6)。
(4)客户端A发回ACK报文确认,并将确认序号设置为收到序号加1(报文段7)。
Linux에서의 SOCKET 프로그래밍에 대한 자세한 설명如图:
过程如下:
某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;
另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;
一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;
接收到这个FIN的源发送端TCP对它进行确认。
这样每个方向上都有一个FIN和ACK。
1.为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?
这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建连请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。
2.为什么TIME_WAIT状态还需要等2MSL后才能返回到CLOSED状态?
这是因为虽然双方都同意关闭连接了,而且握手的4个报文也都协调和发送完毕,按理可以直接回到CLOSED状态(就好比从SYN_SEND状态到ESTABLISH状态那样);但是因为我们必须要假想网络是不可靠的,你无法保证你最后发送的ACK报文会一定被对方收到,因此对方处于LAST_ACK状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT状态的作用就是用来重发可能丢失的ACK报文。
7. Socket编程实例
服务器端:一直监听本机的8000号端口,如果收到连接请求,将接收请求并接收客户端发来的消息,并向客户端返回消息。
/* File Name: server.c */ #include<stdio.h> #include<stdlib.h> #include<string.h> #include<errno.h> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #define DEFAULT_PORT 8000 #define MAXLINE 4096 int main(int argc, char** argv) { int socket_fd, connect_fd; struct sockaddr_in servaddr; char buff[4096]; int n; //初始化Socket if( (socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1 ){ printf("create socket error: %s(errno: %d)\n",strerror(errno),errno); exit(0); } //初始化 memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//IP地址设置成INADDR_ANY,让系统自动获取本机的IP地址。 servaddr.sin_port = htons(DEFAULT_PORT);//设置的端口为DEFAULT_PORT //将本地地址绑定到所创建的套接字上 if( bind(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){ printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno); exit(0); } //开始监听是否有客户端连接 if( listen(socket_fd, 10) == -1){ printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno); exit(0); } printf("======waiting for client's request======\n"); while(1){ //阻塞直到有客户端连接,不然多浪费CPU资源。 if( (connect_fd = accept(socket_fd, (struct sockaddr*)NULL, NULL)) == -1){ printf("accept socket error: %s(errno: %d)",strerror(errno),errno); continue; } //接受客户端传过来的数据 n = recv(connect_fd, buff, MAXLINE, 0); //向客户端发送回应数据 if(!fork()){ /*紫禁城*/ if(send(connect_fd, "Hello,you are connected!\n", 26,0) == -1) perror("send error"); close(connect_fd); exit(0); } buff[n] = '\0'; printf("recv msg from client: %s\n", buff); close(connect_fd); } close(socket_fd); }
客户端:
/* File Name: client.c */ #include<stdio.h> #include<stdlib.h> #include<string.h> #include<errno.h> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #define MAXLINE 4096 int main(int argc, char** argv) { int sockfd, n,rec_len; char recvline[4096], sendline[4096]; char buf[MAXLINE]; struct sockaddr_in servaddr; if( argc != 2){ printf("usage: ./client <ipaddress>\n"); exit(0); } if( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){ printf("create socket error: %s(errno: %d)\n", strerror(errno),errno); exit(0); } memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(8000); if( inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0){ printf("inet_pton error for %s\n",argv[1]); exit(0); } if( connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){ printf("connect error: %s(errno: %d)\n",strerror(errno),errno); exit(0); } printf("send msg to server: \n"); fgets(sendline, 4096, stdin); if( send(sockfd, sendline, strlen(sendline), 0) < 0) { printf("send msg error: %s(errno: %d)\n", strerror(errno), errno); exit(0); } if((rec_len = recv(sockfd, buf, MAXLINE,0)) == -1) { perror("recv error"); exit(1); } buf[rec_len] = '\0'; printf("Received : %s ",buf); close(sockfd); exit(0); }
inet_pton 是Linux下IP地址转换函数,可以在将IP地址在“点分十进制”和“整数”之间转换 ,是inet_addr的扩展。
int inet_pton(int af, const char *src, void *dst);//转换字符串到网络地址:
第一个参数af是地址族,转换后存在dst中
af = AF_INET:src为指向字符型的地址,即ASCII的地址的首地址(ddd.ddd.ddd.ddd格式的),函数将该地址转换为in_addr的结构体,并复制在*dst中
af =AF_INET6:src为指向IPV6的地址,函数将该地址转换为in6_addr的结构体,并复制在*dst中
如果函数出错将返回一个负值,并将errno设置为EAFNOSUPPORT,如果参数af指定的地址族和src格式不对,函数将返回0。
测试:
编译server.c
gcc -o server server.c
启动进程:
./server
显示结果:
======waiting for client's request======
并等待客户端连接。
编译 client.c
gcc -o client server.c
客户端去连接server:
./client 127.0.0.1
等待输入消息
发送一条消息,输入:c++
此时服务器端看到:
客户端收到消息:
其实可以不用client,可以使用telnet来测试:
telnet 127.0.0.1 8000
注意:
在ubuntu 编译源代码的时候,头文件types.h可能找不到。
使用dpkg -L libc6-dev | grep types.h 查看。
如果没有,可以使用
apt-get install libc6-dev安装。
如果有了,但不在/usr/include/sys/目录下,手动把这个文件添加到这个目录下就可以了。