Linux에서 소켓 서버 및 클라이언트의 간단한 구현

목차

1. Linux 파일 설명자

2. Linux에서 소켓 만들기

3. bind () 함수와 connect () 함수

3.1, bind () 함수

3.2, connect () 함수

4. listen () 함수와 accept () 함수

4.1, listen () 함수

4.2, accept () 함수

5 、 write () 和 read ()

5.1, write () 함수

5.2, read () 함수

6, send () 및 recev ()

7, 서비스 및 클라이언트의 간단한 구현


1. Linux 파일 설명자

Linux에서는 모든 것이 파일입니다. 하드웨어 장치는 장치 파일이라고하는 가상 파일에 매핑 될 수도 있습니다. 예를 들어, stdin은 표준 입력 파일이라고하며 해당 하드웨어 장치는 일반적으로 키보드, stdout은 표준 출력 파일, 해당 하드웨어 장치는 일반적으로 디스플레이입니다. 모든 파일에 대해 read () 함수를 사용하여 데이터를 읽고 write () 함수를 사용하여 데이터를 쓸 수 있습니다.

"모든 것이 파일이다"라는 아이디어는 프로그래머의 이해와 조작을 크게 단순화하여 일반 파일과 같은 하드웨어 장치 처리를 만듭니다. Linux에서 생성 된 모든 파일에는 File Descriptor라는 int 유형 번호가 있습니다. 파일을 사용할 때 파일 설명 자만 알면됩니다. 예를 들어, stdin에 대한 설명자는 0이고 stdout에 대한 설명자는 1입니다.

Linux에서 소켓은 일반적인 파일 작업과 다르지 않은 일종의 파일로 간주되기 때문에 네트워크 데이터 전송 과정에서 파일 I / O 관련 기능을 자연스럽게 사용할 수 있습니다. 두 컴퓨터 간의 통신은 실제로 두 소켓 파일의 상호 읽기 및 쓰기라고 볼 수 있습니다.

파일 설명자는 파일 핸들이라고도하지만 "핸들"은 주로 Windows에서 사용되는 용어입니다.

2. Linux에서 소켓 만들기

Linux에서는 <sys / socket.h> 헤더 파일의 socket () 함수를 사용하여 소켓을 만듭니다. 프로토 타입은 다음과 같습니다.

int socket(int af, int type, int protocol);
  • af : 주소 계열, 즉 IP 주소 유형, 일반적으로 사용되는 AF_INET 및 AF_INET6. AF는 "Address Family"의 줄임말이고 INET은 "Internet"의 줄임말입니다. AF_INET은 IPv4 주소를 나타냅니다. 예를 들어, 127.0.0.1; AF_INET6은 1030 :: C9B4 : FF12 : 48AA : 1A2B와 같은 IPv6 주소를 나타냅니다. PF 접두사를 사용할 수도 있습니다. PF는 AF와 동일한 "Protocol Family"의 약자입니다. 예를 들어 PF_INET은 AF_INET에 해당하고 PF_INET6은 AF_INRT6에 해당합니다.
  • 유형 : 데이터 전송 방법, 일반적으로 사용되는 것은 SOCK_STREAM 및 SOCK_DGRAM 입니다.
  • 프로토콜은 전송 프로토콜을 나타내며 일반적으로 사용되는 프로토콜은 각각 TCP 전송 프로토콜과 UDP 프로토콜을 나타내는 IPPROTO_TCP 및 IPPTOTO_UDP입니다.

이것을보고 궁금한 점이있을 수 있는데, IP 주소의 종류와 데이터 전송 방식으로 어떤 프로토콜을 사용할지 결정하는 것만으로는 충분하지 않습니까? 세 번째 매개 변수가 필요한 이유는 무엇입니까?

맞습니다. 정상적인 상황에서는 두 개의 매개 변수 af와 type을 사용하여 소켓을 만들 수 있으며, 이러한 상황이 발생하지 않는 한 운영 체제는 자동으로 프로토콜 유형을 추론합니다. 동일한 IP 주소 유형을 지원하는 두 가지 프로토콜이 있습니다. 데이터 전송 방법. 사용할 프로토콜을 모르는 경우 운영 체제에서 자동으로 공제 할 수 없습니다.

af 값을 PF_INET으로 설정하고 SOCK_STREAM 전송 방법을 사용하면이 두 조건을 충족하는 유일한 프로토콜은 TCP이므로 SOCK () 함수는 다음과 같이 호출 할 수 있습니다.

int tcp_socket = socket(AD_INET,SOCK_STREAM,IPPROTO_TCP);  //TCP套接字

af의 값을 PF_INET으로 설정하고 SOCK_DGRAM 전송 방법을 사용하면이 두 조건을 충족하는 유일한 프로토콜은 UDP이므로 SOCKET () 함수는 다음과 같이 호출 할 수 있습니다.

int udp_socket = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP); //UDP套接字

위의 두 경우에서 하나의 프로토콜 만 조건을 만족하며 프로토콜의 값을 0으로 설정하면 어떤 프로토콜을 사용해야하는지 시스템이 자동으로 추론합니다. 코드는 다음과 같습니다.

int tcp_socket = socket(AD_INET,SOCK_STREAM,0);  //TCP套接字

int udp_socket = socket(AF_INET,SOCK_DGRAM,0); //UDP套接字

3. bind () 함수와 connect () 함수

socket () 함수는 소켓을 생성하고 소켓의 다양한 속성을 결정하는 데 사용되며, 서버 측은 bind () 함수를 사용하여 소켓을 특정 IP 주소 및 포트에 바인딩해야합니다. IP 주소와 포트의 데이터는 소켓으로 전달 될 수 있으며 클라이언트는 연결을 설정하기 위해 connect () 함수를 사용해야합니다.

3.1, bind () 함수

bind () 함수의 프로토 타입은 다음과 같습니다.

int bind(int sock,struct sockaddr *addr,socklen_t addrlen);

sock는 소켓 파일 설명자, addr은 sockaddr 구조 변수 포인터, addrlen은 addr 변수의 크기입니다.

socklen_t의 정의는  실제로 uint32입니다.

생성 된 소켓을 IP 주소 128.0.0.1 및 포트 1123에 바인딩하는 코드를 살펴 보겠습니다.

int serv_sock = sock(AF_INET,SOCK_STREAM,IPPROTO);  //创建一个TCP套接字

struct sockaddr_in serv_addr;
memset(&serv_addr,0,sizeof(servaddr));
serv_addr.sin_famil = AF_INET;  //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
serv_addr.sin_port = htons(1123);  //端口

bind(serv_sock,(struct sockaddr*) &serv_addr,sizeof(serv_addr));

sockaddr_in 구조를 살펴 보겠습니다.

struct sockaddr_in
{
    sa_family_t    sin_family;  //地址族(Address Family),也就是地址类型
    unit16_t       sin_port;    //16位的端口号
    struct in_addr sin_addr;   //32位IP地址
    char           sin_zero[8]; //不使用,一般用0填充
}
  1. sin_family와 socket ()의 첫 번째 매개 변수의 의미는 동일하며 값은 동일해야합니다.
  2. sin_port는 포트 번호이고 uint16_t의 길이는 2 바이트입니다. 이론적으로 포트 번호의 값 범위는 0 ~ 65536이지만 일반적으로 포트 0 ~ 1023은 시스템에서 특정 서비스 프로그램에 할당됩니다. 웹 서비스 포트는 70, FTP 서비스 포트는 21이므로 우리 프로그램은 1024 ~ 65536 사이의 포트 번호를 할당하려고합니다.
  3. sin_addr은 struct in_addr 구조 유형의 변수입니다.
  4. sin_zero는 8 바이트 이상으로 쓸모가 없으며 일반적으로 memset () 함수를 사용하여 0으로 채워집니다. 위 코드에서 먼저 memset ()을 사용하여 구조체의 모든 바이트를 0으로 채운 다음 처음 세 멤버에 값을 할당합니다. 나머지 sin_zero는 당연히 0입니다.

in_addr 구조

sockaddr_in의 세 번째 멤버는 아래와 같이 하나의 멤버 만 포함하는 in_addr 유형의 구조입니다.

struct in_addr
{
    in_addr_t s_addr;  //32位的IP地址
};

in_addr_t는 헤더 파일 <netinet / in.h>에 정의되어 있으며 unsigned long과 동일하며 길이는 4 바이트입니다. 즉, s_addr은 정수이고 IP 주소는 문자열이므로 변환에는 inet_addr () 함수가 필요합니다. 예를 들면 다음과 같습니다.

unsigned long ip = inet_addr("127.0.0.1");

작업 결과 :

sockaddr 대신 sockaddr_in을 사용하는 이유는 무엇입니까?

bind ()의 두 번째 매개 변수 유형은 sockaddr이지만 코드에서 sockaddr_in이 사용 된 후 강제로 sockaddr로 지정됩니다.

sockaddr 구조의 정의는 다음과 같습니다.

struct sockaddr
{
    sa_family_t sin_family;   //地址族
    char        sa_data[14];  //IP地址和端口号
}

아래 그림은 sockaddr과 sockaddr_in을 비교 한 것입니다 (괄호 안의 숫자는 점유 된 바이트 수를 나타냄).

sockaddr과 sockaddr_in의 길이는 같고 둘 다 16 바이트이지만 sockaddr의 sa_data 영역은 "127.0.0.1:8080"과 같이 IP 주소와 포트 번호를 동시에 지정해야합니다. 이 문자열을 필수로 변환하는 관련 함수가 없습니다. sockaddr 유형의 변수에 값을 직접 할당하는 것이 어렵 기 때문에 대신 sockaddr_in을 사용하십시오. 이 두 구조의 길이는 동일하며 유형이 캐스트 될 때 바이트가 손실되지 않으며 더 이상 바이트가 없습니다.

sockaddr은 여러 유형의 IP 주소와 포트 번호를 보호하는 데 사용할 수있는 일반적인 구조이고 sockaddr_in은 IPv4를 저장하는 데 특별히 사용되는 구조라고 볼 수 있습니다. 또한 IPv6 주소를 저장하는 데 사용되는 sockaddr_in6이 있으며 정의는 다음과 같습니다.

struct sockaddr_in6
{
    sa_family_t sin6_family;   //IP地址类型,取值为AF_INET6
    in_port_t   sin6_port;     //16位端口号
    uint32_t sin6_flowinfo;    //IPv6流信息
    struct in6_addr sin6_addr; //具体的IPv6地址
    unit32_t sin6_scpoe_id;    //接口范围ID
};

in.h에 선언 된 sockaddr_in 및 sockaddr_in6은 다음과 같습니다. 

3.2, connect () 함수

connect () 함수는 연결을 설정하는 데 사용되며 프로토 타입은 다음과 같습니다.

int connect(int sock,struct sockaddr *serv_addr,struct sockaddr*serv_addr,socklen_t addrlen);

각 매개 변수의 설명은 bind () 함수와 동일합니다.

4. listen () 함수와 accept () 함수

서버 측 프로그램의 경우 bind ()를 사용하여 소켓을 바인드 한 후 listen () 함수를 사용하여 소켓이 수동 청취 상태로 들어가도록 한 다음 accept () 함수를 호출하여 클라이언트의 응답에 응답해야합니다. 언제든지 요청하십시오.

4.1, listen () 함수

listen () 함수는 소켓이 수동적 인 청취 상태로 들어갈 수 있도록합니다. 프로토 타입은 다음과 같습니다.

int listen(int sock,int backlog)

sock는 청취 상태로 들어가야하는 소켓이고 백로 그는 요청 큐의 최대 길이입니다.

소위 수동 모니터링은 클라이언트 요청이 없을 때 소켓이 "휴면"상태에 있다는 것을 의미합니다. 클라이언트 요청이 수신 될 때만 소켓이 "깨어나"요청에 응답합니다.

요청 대기열

소켓이 클라이언트 요청을 처리 중일 때 새 요청이 들어 오면 소켓을 처리 할 수 ​​없으며 먼저 버퍼에 배치 한 다음 현재 요청이 처리 된 후 버퍼에서 처리를 위해 읽습니다. 새로운 요청이 들어 오면 버퍼가 가득 찰 때까지 순서대로 버퍼에 대기합니다. 이 버퍼를 요청 큐라고합니다.

버퍼의 길이 (저장할 수있는 클라이언트 요청 수)는 listen () 함수의 backlog 매개 변수로 지정할 수 있지만 크기에 대한 표준은 없으며 필요에 따라 다릅니다.

백 로그 값을 SOMAXCONN으로 설정하면 시스템이 요청 대기열의 길이를 결정하며 일반적으로이 값은 비교적 크고 수백 이상일 수 있습니다.

요청 대기열이 가득 차면 새로운 요청이 수신되지 않으며 Linux의 경우 클라이언트는 ECONNREFUSED 오류를 수신합니다.

참고 : listen () 함수는 소켓을 청취 상태로 유지하고 요청을 수신하지 않습니다. 요청을 받으려면 accept () 함수를 사용하여 새 요청이 올 때까지 프로세스 실행을 차단해야합니다.

4.2, accept () 함수

소켓이 수신 상태 일 때 acceot () 함수를 통해 클라이언트 요청을받을 수 있습니다. 프로토 타입은 다음과 같습니다.

int accept(int socket,struct sockaddr *addr,socklen_t *addrlen);

매개 변수는 listen () 및 connect ()와 동일합니다 .sock은 서버 측 소켓, addr은 sockaddr_in 구조 변수, addrlen은 매개 변수 addr의 길이이며 sizeof ()로 얻을 수 있습니다.

accept ()는 클라이언트와 통신하기 위해 새 소켓을 반환하고, addr는 클라이언트의 IP 주소와 포트 번호를 저장하며, sock는 서버 측 소켓입니다. 나중에 클라이언트와 통신 할 때 원래 서버 측 소켓 대신 새로 생성 된 소켓을 사용하십시오.

5 、 write () 和 read ()

Linux는 소켓 파일과 일반 파일을 구분하지 않습니다. 소켓에 데이터를 쓰려면 write ()를 사용하고 소켓에서 데이터를 읽으려면 read ()를 사용하십시오.

두 컴퓨터 간의 통신은 두 소켓 간의 통신과 동일합니다. 서버에서 write ()를 사용하여 소켓에 데이터를 쓰고 클라이언트가 데이터를 수신 한 다음 read ()를 사용하여 소켓에서 연결합니다. 단어를 읽은 후 , 통신이 완료되었습니다.

5.1, write () 함수

write ()의 프로토 타입은 다음과 같습니다.

/*
fd:待写入的文件的描述符
buf:待写入的数据的缓冲区地址
nbytes:写入的数据的字节数
ssize_t:signed int
*/
ssize_t write(int  fd,const void *buf,size_t nbytes);

write () 함수는 버퍼 buf의 nbytes 바이트를 fd 파일에 쓰고 성공하면 쓴 바이트 수를 반환하고 실패하면 -1을 반환합니다.

5.2, read () 함수

/*
fd:待读取的文件的描述符
buf:待读取的数据的缓冲区地址
nbytes:读取的数据的字节数
ssize_t:signed int
*/
ssize_t read(int  fd,void *buf,size_t nbytes);

read () 함수는 fd 파일에서 nbytes 바이트를 읽어 버퍼 buf에 저장합니다. 성공하면 읽은 바이트 수를 반환하고 (파일의 끝을 발견하면 0 반환) 실패하면 -1을 반환합니다. .

6, send () 및 recev ()

/* Send N bytes of BUF to socket FD.  Returns the number sent or -1.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t send (int __fd, const void *__buf, size_t __n, int __flags);

/* Read N bytes into BUF from socket FD.
   Returns the number read or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t recv (int __fd, void *__buf, size_t __n, int __flags);

/* Send N bytes of BUF on socket FD to peer at address ADDR (which is
   ADDR_LEN bytes long).  Returns the number sent, or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t sendto (int __fd, const void *__buf, size_t __n,
                       int __flags, __CONST_SOCKADDR_ARG __addr,
                       socklen_t __addr_len);

/* Read N bytes into BUF through socket FD.
   If ADDR is not NULL, fill in *ADDR_LEN bytes of it with tha address of
   the sender, and store the actual size of the address in *ADDR_LEN.
   Returns the number of bytes read or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t recvfrom (int __fd, void *__restrict __buf, size_t __n,
                         int __flags, __SOCKADDR_ARG __addr,
                         socklen_t *__restrict __addr_len);


/* Send a message described MESSAGE on socket FD.
   Returns the number of bytes sent, or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t sendmsg (int __fd, const struct msghdr *__message,
                        int __flags);

#ifdef __USE_GNU
/* Send a VLEN messages as described by VMESSAGES to socket FD.
   Returns the number of datagrams successfully written or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int sendmmsg (int __fd, struct mmsghdr *__vmessages,
                     unsigned int __vlen, int __flags);
#endif

/* Receive a message as described by MESSAGE from socket FD.
   Returns the number of bytes read or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t recvmsg (int __fd, struct msghdr *__message, int __flags);

#ifdef __USE_GNU
/* Receive up to VLEN messages as described by VMESSAGES from socket FD.
   Returns the number of messages received or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int recvmmsg (int __fd, struct mmsghdr *__vmessages,
                     unsigned int __vlen, int __flags,
                     struct timespec *__tmo);
#endif

7, 서비스 및 클라이언트의 간단한 구현

/*================================================================
 *   Copyright (C) 2021 baichao All rights reserved.
 *
 *   文件名称:service.c
 *   创 建 者:baichao
 *   创建日期:2021年01月22日
 *   描    述:
 *
 ================================================================*/

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

int main(){
    //创建套接字
    int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

    //将套接字和IP、端口绑定
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));  //每个字节都用0填充
    serv_addr.sin_family = AF_INET;  //使用IPv4地址
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具体的IP地址
    serv_addr.sin_port = htons(1234);  //端口
    bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

    //进入监听状态,等待用户发起请求
    listen(serv_sock, 20);

    //接收客户端请求
    struct sockaddr_in clnt_addr;
    socklen_t clnt_addr_size = sizeof(clnt_addr);
    while(1)
    {
        int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);

        //向客户端发送数据
        char str[] = "不要艾特我";
        write(clnt_sock, str, sizeof(str));

        //关闭套接字
        close(clnt_sock);
    }
    close(serv_sock);
    return 0;
}
/*================================================================
 *   Copyright (C) 2021 baichao All rights reserved.
 *
 *   文件名称:client.cpp
 *   创 建 者:baichao
 *   创建日期:2021年01月22日
 *   描    述:
 *
 ================================================================*/

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

int main(){
    //创建套接字
    int sock = socket(AF_INET, SOCK_STREAM, 0);

    //向服务器(特定的IP和端口)发起请求
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));  //每个字节都用0填充
    serv_addr.sin_family = AF_INET;  //使用IPv4地址
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具体的IP地址
    serv_addr.sin_port = htons(1234);  //端口
    connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

    //读取服务器传回的数据
    char buffer[40];
    read(sock, buffer, sizeof(buffer)-1);

    printf("Message form server: %s\n", buffer);

    //关闭套接字
    close(sock);

    return 0;
}

작업 결과 :

서버 시작

서버가 모니터링 상태입니다.

클라이언트를 시작하십시오.

이 시점에서 간단한 소켓 통신 코드가 완성되었습니다.

 

 

 

 

 

 

 

추천

출처blog.csdn.net/weixin_40179091/article/details/113024907