TCP/IP网络编程(一)

时间:2021-12-03 10:17:25

这一系列博客将用于记录学习《TCP/IP网络编程》的笔记。
先上代码。下面是服务器端的代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//提供针对系统调用的封装
#include <unistd.h>
//提供用于网络字节序转换的函数
#include <arpa/inet.h>
#include <sys/socket.h>

//错误信息展示
void error_handling (char *message);

int main(int argc, char *argv[]){
int serv_sock, clnt_sock;
struct sockaddr_in serv_addr, clnt_addr;
socklen_t clnt_addr_size;

char message[] = "Hello Word!";

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_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));

if (bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("bind() error");

if (listen(serv_sock, 5) == -1)
error_handling("Listen() Error");

clnt_addr_size = sizeof(clnt_addr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
if (clnt_sock == -1)
error_handling("Acceot Error");

write(clnt_sock, message, sizeof(message));
close(clnt_sock);
close(serv_sock);

return 0;
}

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

下面是客户端的代码

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

//错误信息展示
void error_handling (char *message){
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}

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

if (argc != 3){
printf ("Uasge : %s <IP> <PORT>", argv[0]);
exit(1);
}

sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock == -1)
error_handling("Socket Error");

memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));

if (connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("Connect Error");

str_len = read(sock, message, sizeof(message)) - 1;
if (str_len == -1)
error_handling("Read Error");

printf("Message Is %s\n", message);
close(sock);

return 0;
}

创建socket

对于Linux系统而言,一切皆文件。所以我们一开始的时候就要创建一个文件,也就是我们的socket。有了文件,才能对文件进行各种各样千奇百怪的操作才是嘛,所以我们创建一个socket文件

int socket(int domain, int type, int protocol)

socket函数会返回一个描述符,也就是一个int整形数字,在Linux下,这个数字就是与系统沟通好的,用来描述当前socket的一个暗号。

描述socket

有了这个socket文件之后呢,我们需要告诉操作系统,这个文件的一些特征。socket函数大概就类似于告诉系统,我要创建一个mp3文件还是一个txt文件。而接下来的这段代码,就是要告诉系统,这个有多大,它可以接受怎样的内容

    memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));

我们先将文件给清空,然后再一一告诉系统这个文件相关的东西。

  • AF_INETIPv4 网络协议的套接字类型
  • htonl(INADDR_ANY) 这里告诉系统,这个socket监听所有的网卡,就是指定地址为0.0.0.0的地址
  • htons(atoi(argv[1]))这里告诉系统,监听哪一个端口,该端口由我们指定

其中htonshtons代表了将本地的字符串字节序转化为网络字节序,防止因为本地的大小端存放问题导致出错。

进行绑定

普通的文件创建完了之后,就可以丢那里不管,需要的时候直接写入数据就可以,但毕竟socket不是一般的文件,它需要从网络上来获取数据,所以我们还需要一步绑定的操作,来告诉操作系统,我这个socket需要监听来自xxxIP,yyy端口的东西啦。这样当有数据从指定的地址端口来的时候,系统才知道,要送到这个socket这里。
我们使用bind函数进行绑定

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

通过这个函数,我们就可以绑定到操作系统上,从而接收数据。

开始监听

绑定完了之后,我们需要让socket去监听丫。bind只是绑定了端口,但是socket并没有对这个端口做什么操作,来了数据也不知道,是吧。
使用listen来监听我们指定的端口

int listen(int sockfd, int backlog)

通过这个操作,当有东西来了的时候,socket就能够知道,并且能够进行相应的处理。

受理连接

对于一个socket而言,它不一定是给指定的连接使用的,它大部分情况下都是能够接收很多很多的连接,那我们怎么去区分这不同的连接,跟不同的连接做交互呢?所以需要一个函数来负责获取这些连接,将每一个连接打上不同的记号,方便我们进行处理。因此,就有了accept函数

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)

很明显,系统也将不同的连接当成了文件,返回了一个个的文件描述符。因此我们能够根据这不同的文件描述符去区分不同的连接。
注意,当socket没有接收到连接的时候,它会阻塞到这里,直到有连接进来或者出现什么失败为止。

连接服务器

对于客户端的代码而言,基本上都和服务器端的代码相同,但是有个不一样的函数出现在这。对于客户端而言,我需要主动的去找服务器,所以客户端的代码需要一个连接的操作

int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addrlen)

通过connect函数,我们能够连接到想要的服务器,跟其进行通信。

关闭socket

对于任一打开的操作,基本上都要记得关闭,不然就可能出大问题!!

int close(int fd)

这个函数非常简单,传入我们的socket描述符,系统便能够将这个socket
相关的东西断开,方便下次使用。

写入数据

既然有了网络连接,自然是需要传递数据的。我们把socket当成了文件看待,所以像其中写入数据自然是使用write函数

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

这个函数的返回值有点奇怪,它是一种元数据类型(primitive),这里与C语言的语言特点有关,C并未规定int, long short这些数据类型的大小,而且操作系统也在不断的发生变化,由最初的16位发展到现在的64位,所以如果我们直接使用int类型的话,可能在之后的代码中需要进行不断的修改,并且可能在某一系统上,int的大小类型并不符合我们的要求,所以我们利用typedef进行额外的定义,定义出一系列大小规整的类型。

读取数据

读取数据与写入数据很类似,都是很标准的Linux文件操作

ssize_t read(int fd, void *buf, size_t nbytes)

通过这个函数,我们能够读取到我们需要的大小的数据

总结

简而言之,创建一个基本的socket还是比较简单的,过程可能有些繁琐。可以归纳为以下几步。

  • 第一步:调用socket创建一个socket
  • 第二步:调用bind函数绑定IP地址和端口号,绑定到操作系统
  • 第三步:调用listen开始转为可接收请求状态
  • 第四步:调用accept函数受理连接请求
    • 客户端需要一个connect函数来连接到服务器
  • 利用writeread进行数据的读写操作
  • 关闭socket