LINUX 기반 TCP 통신

머리말

TCP는 연결 지향적이고 안전하며 반복적이지 않으며 질서 정연합니다.
서버 초기화 프로세스 : socket () ----> bind () ----> listen () ----> accept ()
클라이언트 초기화 프로세스 : socket () ----> connect ()

많은 인터페이스가 사용되고 있음을 확인하고 작성하기 전에 다양한 네트워크 프로그래밍 API, 각 매개 변수의 역할, 사용 순서 및 제한 사항을 이해해야합니다.

네트워크 프로그래밍 인터페이스 API

1. 소켓

int socket(int family, int type, int protocol);

socket ()은 소켓을 생성합니다. 성공하면 open ()과 같은 파일 설명자를 반환합니다. 응용 프로그램은 읽기 / 쓰기를 사용하여 파일 읽기 및 쓰기와 같이 네트워크에서 데이터를 보내고받을 수 있습니다. socket () 호출이 실패하면 -1을 반환합니다. IPv4의 경우 패밀리 매개 변수는 AF_INET으로 지정됩니다. TCP 프로토콜의 경우 type 매개 변수는 스트림 지향 전송 프로토콜을 나타내는 SOCK_STREAM으로 지정됩니다. UDP 프로토콜 인 경우 type 매개 변수는 데이터 그램 지향 전송 프로토콜을 나타내는 SOCK_DGRAM을 지정합니다. 프로토콜 매개 변수는 0으로 지정할 수 있습니다.

2. 바인딩

int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);

bind ()의 기능은 sockfd와 myaddr 매개 변수를 함께 묶어 네트워크 통신에 사용되는 파일 설명자인 sockfd
가 myaddr에 의해 설명 된 주소와 포트 번호를 모니터링하도록하는 것입니다. 앞에서 언급했듯이 struct sockaddr *는 범용 포인터 유형입니다 .myaddr 매개 변수는 실제로 여러 프로토콜의 sockaddr 구조를 수용 할 수 있으며 길이가 다르므로 세 번째 매개 변수 인 addrlen이 구조의 길이를 지정합니다.

struct sockaddr_in server_addr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
bind(sockfd, (struct sockaddr *)(&server_addr), sizeof(struct sockaddr))

구조 변수 struct sockaddr_in server_addr에 대해 설명하겠습니다 . 초기화 후 소켓에 바인딩해야하기 때문입니다. bind 함수에서 struct sockaddr * 매개 변수 포인터는 이전 것과 다릅니다.이 두 구조를 소개하겠습니다. 본문 차이점 :
1 )
/usr/include/bits/socket.h 아래의 sockaddr sockaddr에서 sockaddr 의 구조를 확인하십시오.

struct sockaddr {
    
    
	sa_family_t sa_family;
	char sa_data[14];
};

sockaddr : sa_data의 결함은 대상 주소와 포트 정보를 혼합합니다. sockaddr_in은 포트 번호와 IP 주소를 별도로 저장하여이 결함을 해결합니다.
2) sockaddr_in
sockaddr_in은 /usr/include/netinet/in.h 아래에 있습니다. sockaddr_in의 구조를 확인하십시오.

struct sockaddr_in {
    
    
	sa_family_t sin_family;
	in_port_t sin_port;
	struct in_addr sin_addr;
	uint8_t sin_zero[8];//用于填充的0字节
};

3) sockaddr_in과 sockaddr의 차이와 연결 :
여기에 사진 설명 삽입
연결 : 둘이 차지하는 메모리 크기가 같기 때문에 서로 변환 할 수 있다는 점에서 차이가 없다.
차이점 : sockaddr은 주소 정보를 표시하기 위해 bind, connect, recvfrom, sendto 및 기타 기능의 매개 변수에 자주 사용되며 범용 소켓 주소입니다.
그리고 sockaddr_in은 인터넷 환경에서 소켓의 주소 형식입니다. 따라서 네트워크 프로그래밍에서는 sockaddr_in 구조를 작동합니다. sockaddr_in을 사용하여 필요한 정보를 만들고 마지막으로 유형 변환을 사용 합니다 .
4) 네트워크 바이트 순서와 호스트 바이트 순서
초기화에서 우리는 두 개의 인터페이스 htonl ()과 htons ()를 사용했습니다. 방법에 따라 두 가지 더 설명하십시오 :
1. 호스트 바이트 순서
는 우리가 보통 빅 엔디안과 리틀 엔디안이라고 부르는 것입니다. 모드에서 큰 끝은 상위 바이트를 저장하는 하위 주소이고 작은 끝은 하위 바이트를 저장하는 하위 주소입니다. CPU마다 엔디안 유형이 다릅니다. 이러한 엔디안은 정수가 메모리에 저장되는 순서를 의미하며이를 호스트 순서라고합니다.
2. 네트워크 바이트 순서
메모리 주소는 큰 끝과 작은 끝으로 나뉘며 네트워크 데이터 스트림도 큰 끝과 작은 끝으로 나뉩니다. 송신 호스트는 일반적으로 메모리 주소가 낮은 순서에서 높은 순서로 송신 버퍼의 데이터를 보내고, 수신 호스트는 네트워크에서 수신 한 바이트를 수신 버퍼에 순서대로 저장합니다. 또한 메모리 주소가 낮은 순서로 저장됩니다. 높은 곳에 저장하세요. 따라서 네트워크 데이터 스트림의 주소는 다음과 같이 지정해야합니다. 먼저 전송 된 데이터는 하위 주소이고 나중에 전송되는 데이터는 상위 주소입니다.
예 : 4 바이트의 32 비트 값이 먼저, 0 ~ 7 비트, 두 번째, 8 ~ 15 비트, 16 ~ 23 비트, 마지막으로 24-31 비트 순서로 전송됩니다. 이것은 빅 엔디안입니다. 즉, TCP / IP 헤더의 모든 이진 정수는 네트워크에서 전송되어야합니다.

호스트 바이트 순서와 네트워크 바이트 순서 간의 변환 기능 :

#include <arpa/inet.h>
/*将32位的长整数从主机字节序转换为网络字节序,*/ 
uint32_t htonl(uint32_t hostlong);
 /*将16位的短整数从主机字节序转换为网络字节序,*/
  uint16_t htons(uint16_t hostshort); 
 /*将32位的长整数从网络字节序转换为主机字节序,*/
  uint32_t ntohl(uint32_t netlong); 
 /*将16位的短整数从网络字节序转换为主机字节序,*/ 
 uint16_t ntohs(uint16_t netshort);

이런 식으로 h는 호스트 (로컬 호스트), n은 net (네트워크), l은 unsigned long (unsigned long)입니다.
리틀 엔디안이면이 함수는 매개 변수를 빅 엔디안으로 변환하고 반환하고, 빅 엔디안이면 변환없이 직접 반환합니다.
프로그래밍을 단순화하기 위해 일반적으로 IP 주소를 INADDR_ANY로 설정합니다. 특정 IP 주소를 사용해야하는 경우 inet_addr 및 inet_ntoa를 사용하여 문자열 및 in_addr 구조의 교환을 완료해야합니다. in_addr은 SOCKADDR_IN의 멤버입니다. IP 주소.
문자열 유형을 만날 때 다음을 사용할 수 있습니다.

 //将字符串转换为in_addr类型  
 sock.sin_addr.S_un.S_addr =  inet_addr("192.168.1.111");  
 sock.sin_port = htons(5000);  
 //将in_addr类型转换为字符串  
 printf("inet_ntoa ip = %s\n",inet_ntoa(sock.sin_addr));

현재 우리는 초기화 상황을 이해합니다.

3. 듣기

int listen(int sockfd, int backlog);

일반적인 서버 프로그램은 동시에 여러 클라이언트에 서비스를 제공 할 수 있습니다. 클라이언트가 연결을 시작하면 서버가 호출 한 accept ()가 연결을 반환하고 수락합니다. 클라이언트가 많으면 연결을 시작할 수 있습니다. 처리하지 않은 고객, 아직 수락하지 않은 고객 클라이언트가 연결 대기 상태에 있고 listen ()은 sockfd가 수신 대기 상태에 있으며 최대 백 로그 클라이언트가 연결 대기 상태에있을 수 있음을 선언합니다. 요청이 수신되면 무시됩니다. listen ()은 성공하면 0을, 실패하면 -1을 반환합니다.

4. 수락

int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);

3 방향 핸드 셰이크가 완료된 후 서버는 accept ()를 호출하여 연결을 수락하고, 서버가 accept ()를 호출 할 때 클라이언트 연결 요청이 없으면 차단하고 클라이언트가 연결될 때까지 대기합니다. cliaddr은 나가는 매개 변수이며 accept ()가 반환되면 클라이언트의 주소와 포트 번호가 전송됩니다. addrlen 매개 변수는 수신 및 발신 매개 변수 (값-결과 인수), 버퍼 오버 플로우 문제를 피하기 위해 호출자가 제공 한 버퍼 cliaddr의 길이, 클라이언트 주소 구조의 실제 길이 (그렇지 않을 수 있습니다. 호출자가 제공 한 버퍼를 채 웁니다). cliaddr 매개 변수에 NULL을 전달하면 클라이언트 주소에 신경 쓰지 않는다는 의미입니다.

while (1) {
    
    
cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
n = read(connfd, buf, MAXLINE);
......
close(connfd);
}

전체는 루프에서 클라이언트 연결이 처리 될 때마다 while 루프입니다. cliaddr_len은 수신 및 발신 매개 변수이므로 accept ()가 호출 될 때마다 초기 값을 다시 지정해야합니다. accept ()의 listenfd 매개 변수는 이전 청취 파일 기술자이고 accept ()의 반환 값은 또 다른 파일 기술자 connfd입니다. 그 후이 connfd를 통해 클라이언트와 통신하고 마지막으로 connfd를 닫아 연결을 끊습니다. 루프의 시작으로 돌아 가기 listenfd는 여전히 accept 매개 변수로 사용됩니다. accept ()는 파일 설명자를 성공적으로 반환하고 오류시 -1을 반환합니다. 클라이언트는 고정 포트 번호가 필요하지 않으므로 bind ()를 호출 할 필요가 없으며 클라이언트의 포트 번호는 커널에 의해 자동으로 할당됩니다. 클라이언트는 bind ()를 호출 할 수 없지만 포트 번호를 수정하기 위해 bind ()를 호출 할 필요가 없으며 서버는 bind ()를 호출 할 필요가 없지만 서버가 bind (를 호출하지 않는 경우) ), 커널은 자동으로 서버에 수신 대기 포트를 할당합니다. 포트 번호는 서버가 시작될 때마다 다르며 클라이언트가 서버에 연결하려는 경우 문제가 발생합니다.
5. 연결

int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);

클라이언트는 서버에 연결하기 위해 connect ()를 호출해야합니다. connect와 bind의 매개 변수는 같은 형식입니다. 차이점은 bind의 매개 변수는 자신의 주소이고 connect의 매개 변수는 다른 주소의 주소라는 것입니다. 파티.
6. 쓰기

Ssize_t write(int fd,const void *buf,size_t nbytes);

Write 함수는 buf의 nbytes 내용을 파일 설명자에 쓰고 성공하면 쓴 바이트 수를 반환하고 실패하면 -1을 반환하고 errno 변수를 설정합니다. 네트워크 프로그램에서 소켓 파일에 데이터를 쓰기 편하게 설명 할 때 두 가지 가능성이 있습니다.

1、write的返回值大于0,表示写了部分数据或者是全部的数据,这样用一个while循环不断的写入数据,但是循环过程中的buf参数和nbytes参数是我们自己来更新的,也就是说,网络编程中写函数是不负责将全部数据写完之后再返回的,说不定中途就返回了! 

2、返回值小于0,此时出错了,需要根据错误类型进行相应的处理。 

如果错误是EINTR表示在写的时候出现了中断错误,如果是EPIPE表示网络连接出现了问题。

7. 읽기

Ssize_t read(int fd,void *buf,size_t nbyte);

Read 함수는 fd에서 내용을 읽는 역할을합니다. 읽기에 성공하면 실제로 읽은 바이트 수를 반환합니다. 반환 값이 0이면 파일 끝을 읽었 음을 의미합니다. 0보다 읽기 오류입니다.
오류가 EINTR이면 쓰기 중 인터럽트 오류가 발생했음을 의미하고 EPIPE이면 네트워크 연결에 문제가 있음을 의미합니다.
8. 닫기

#include <unistd.h>
int close(int fd);

읽기 및 쓰기를 끕니다.
성공시 0, 오류시 -1, 오류 코드 errno를 반환합니다. EBADF는 fd가 유효한 설명자가 아님을 나타내고 EINTR은 닫기 함수가 신호에 의해 중단되었음을 나타냅니다 .EIO는 IO 오류를 나타냅니다.

서버 측 소켓 프로그래밍

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <netdb.h>
#include <errno.h>
#define PORT 2345
#define MAXSIZE 1024
int main(int argc, char *argv[])
{
    
    
int sockfd, newsockfd;
//定义服务端套接口数据结构
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
int sin_zise, portnumber;
//发送数据缓冲区
char buf[MAXSIZE];
//定义客户端套接口数据结构
int addr_len = sizeof(struct sockaddr_in);
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
    
    
fprintf(stderr, "create socket failed\n");
exit(EXIT_FAILURE);
}
puts("create socket success");
printf("sockfd is %d\n", sockfd);
//清空表示地址的结构体变量
bzero(&server_addr, sizeof(struct sockaddr_in));
//设置addr的成员变量信息
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
//设置ip为本机IP
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//server_addr.sin_addr.s_addr = inet_addr("192.168.69.129"),指定ip创建服务器端
if (bind(sockfd, (struct sockaddr*)(&server_addr), sizeof(struct sockaddr)) < 0)
{
    
    
fprintf(stderr, "bind failed \n");
exit(EXIT_FAILURE);
}
puts("bind success\n");
if (listen(sockfd, 10) < 0)
{
    
    
perror("listen fail\n");
exit(EXIT_FAILURE);
}
puts("listen success\n");
int sin_size = sizeof(struct sockaddr_in);
printf("sin_size is %d\n", sin_size);
if ((newsockfd = accept(sockfd, (struct sockaddr *)(&client_addr), &sin_size)) < 0)
{
    
    
perror("accept error");
exit(EXIT_FAILURE);
}
printf("accepted a new connetction\n");
printf("new socket id is %d\n", newsockfd);
printf("Accept clent ip is %s\n", inet_ntoa(client_addr.sin_addr));
printf("Connect successful please input message\n");
char sendbuf[1024];
char mybuf[1024];
while (1)
{
    
    
int len = recv(newsockfd, buf, sizeof(buf), 0);
if (strcmp(buf, "exit\n") == 0)
break;
fputs(buf, stdout);
send(newsockfd, buf, len, 0);
memset(sendbuf, 0 ,sizeof(sendbuf));
memset(buf, 0, sizeof(buf));
}
close(newsockfd);
close(sockfd);
puts("exit success");
exit(EXIT_SUCCESS);
return 0;
}

클라이언트 소켓 프로그래밍

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <string.h>
#include <errno.h>
#define PORT 2345
int count = 1;
int main()
{
    
    
int sockfd;
char buffer[2014];
struct sockaddr_in server_addr;
struct hostent *host;
int nbytes;
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
    
    
fprintf(stderr, "Socket Error is %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//server_addr.sin_addr.s_addr = inet_addr("192.168.69.129"),指定ip的代码
//客户端发出请求
if (connect(sockfd, (struct sockaddr *)(&server_addr), sizeof(struct sockaddr)) == -1)
{
    
    
fprintf(stderr, "Connect failed\n");
exit(EXIT_FAILURE);
}
char sendbuf[1024];
char recvbuf[2014];
while (1)
{
    
    
fgets(sendbuf, sizeof(sendbuf), stdin);
send(sockfd, sendbuf, strlen(sendbuf), 0);
if (strcmp(sendbuf, "exit\n") == 0)
break;
recv(sockfd, recvbuf, sizeof(recvbuf), 0);
fputs(recvbuf, stdout);
memset(sendbuf, 0, sizeof(sendbuf));
memset(recvbuf, 0, sizeof(recvbuf));
}
close(sockfd);
exit(EXIT_SUCCESS);
return 0;
}

연결을 위해 직접 컴파일 할 수 있습니다. 프로그래밍을 단순화하기 위해 일반적으로 IP 주소를 INADDR_ANY로 직접 대체합니다. 지정된 IP 문자열을 입력하려면 inet_addr () 인터페이스 변환을 사용할 수 있습니다. 이러한 IP의 변환은 다음과 같습니다. 전에 구별되었습니다.

결과 (IP가 192.168.69.129 인 서버가 가상 머신에 있으므로 지정되었으므로 클라이언트 ip = server ip)

여기에 사진 설명 삽입

추천

출처blog.csdn.net/weixin_42271802/article/details/108713114