Socket是封装了TCP协议,让我们更容易使用TCP协议。TCP协议在OSI模型中属于四层协议,即传输层协议。
TCP,中文叫传输控制协议,它是一种面向连接的协议,就是说它通信前必须先连接,再能通信。设计TCP这种协议的目的,是为了实现在网络中传输数据包,所以几乎所有网络编程都会涉及TCP协议,就连HTTP协议也是基于TCP来完成数据的传输的。
说TCP是面向连接还有一层意思,除了在传输之前需要在源端和目的端建立连接之外,它会一直接维持连接的状态。
如果你给一个大数据包TCP传送,TCP会先把这个大数据包,拆成多个小的数据包再传送出去,目的端接收这些小数据后组成这个大数据包,再给到对应的应用程序。
用TCP通信的架构几乎都是客户端-服务端这种模式,在这种模式中,客户端首先主动向服务端发起通信请求,这个请求就是要先和服务端建立连接。
接下来我们会用C语言实现Socket的客户端和服务端。同时我们会价一些C语言的知识。
头文件介绍
-
stdio.h : 这个文件头文件是标准的输入输出,Standard Input Output。这个头文件主要涉及文件相关的输入输出操作。典型的方法printf() , scanf(),getc(), putc()。怎么理解这里文件呢?在Linux,有一个基本的原则,键盘、显示等这些操作都会作为文件来对待。事实上,键盘输入是默认的stdin文件流,显示输出是默认的stdout文件流。
-
stdlib.h:Standard Library标准库,主要涉及内存相关的操作,典型的方法malloc() ,free(),abort(),exit()
-
string.h: 这个头文件涉及了许多字符数组(字符串)的操作,如strlen()
-
unistd.h: 这个是Linux/Unix系统的内置头文件,涉及了许多系统调用的原型,包含了许多标准符号常量和类型,如getuid() setuid() sleep()等等
-
sys/socket.h 这是主要的socket头文件,socket编程都要引入这个头文件。
-
arpa/inet.h 这个头文件涉及了网络操作的定义
Socket 客户端
1.创建socket
short create_socket(){short sock;printf("Create a socket\n");sock = socket(AF_INET,SOCK_STREAM,0);return sock;
}
- 这里用到sys/socket.h头文件中的socket()函数
- AF_INET宏也定义在sys/socket.h头文件里,代表IPv4地址,AF代表了Address Family地址族。类似的还有AF_INET6(IPv6地址)等
- SOCK_STREAM这个宏也定义在sys/socket.h头文件里,它代表的是字节流socket,类似的有SOCK_SEQPACKET(顺序包socket)、SOCK_RAW(原始协议接口)、SOCK_DGRAM(数据报socket)。
这里调用了一个系统调用int socket(int domain, int type, int protocol);
- domain参数指定了一个通信域,选择用于通信的协议族,所有可用的协议族都定义在sys/socket.h,本例选择了AF_INET
- type参数指定socket的类型,这个类型确定了通信的语义。简单地来说就是确定了对一系列符号的理解,通常语义都有特定的环境。举个例子为说吧,假如玩个游戏,出现100就是举左手,出现50就举右手,那么参与这个游戏的人都会根据对应的数字举相应的手。这就是参与游戏人对100和50的意义的理解。在socket中这个type的参数的选择就确定了对数据的意义的理解方式。
- protocol参数指定了特定的用于socket的协议,一般来说在一个给定的协议族中只存在一个协议能够支持特定类型的socket,在这种情况,可以设置为0 。特殊情况下,可能在domain指定的协议族中存在多个协议能够支持特定类型的socket,此时我们可以指定要哪个协议来支持给定类型的socket,通过设置protocol这个参数,来选用协议族中特定的协议。
本例中,创建了一个IPv4协议族的字节流socket。
sock = socket(AF_INET,SOCK_STREAM,0);
2.连接服务端
int connect_socket(int sock) {int ret = -1;int server_port = 60000;struct sockaddr_in remote = {0};//服务器地址remote.sin_addr.s_addr = inet_addr("127.0.0.1");//socket的协议族,这里是IPv4协议族remote.sin_family = AF_INET;//服务器端口号remote.sin_port = htons(server_port);//连接服务器端ret = connect(sock,(struct sockaddr *)&remote,sizeof(struct sockaddr));return ret;
}
sockaddr_in结构体详情
typedef uint32_t in_addr_t;typedef uint16_t in_port_t; // 这就是为什么端口号最大只到65535typedef /* ... */ socklen_t; //socket地址的长度,至少是一个32比特的整型typedef /* ... */ sa_family_t;// 这是一个无符号整型,它用于描述socket的协议族struct in_addr {in_addr_t s_addr;};struct sockaddr {sa_family_t sa_family; /* Address family */char sa_data[]; /* Socket address */};struct sockaddr_in {sa_family_t sin_family; /* AF_INET */in_port_t sin_port; // 端口号struct in_addr sin_addr; /* IPv4 address */};
在arpa/inet.h有两个方法:
- inet_addr()函数将标准IPv4点分十进制格式的字符串转换为适合作为Internet地址使用的整数值。
- htons()函数将返回从主机字节顺序转换为网络字节顺序的参数值。这里就是将端口值转换成网络字节序。
连接socket的函数:int connect(int socket, const struct sockaddr *address, socklen_t address_len);
- socket参数是一个与socket关联的文件描述符。这也是一个说明在linux一切皆文件的概念的例子。因为读取另一端的socket发送过来的数据是通过这个文件描述
- address参数对等端的地址,在这上述代码中这个值就是服务端的地址
- address_len参数指的是address参数的struct sockaddr 结构体的长度。
(struct sockaddr *)&remote
,这里将结构体struct sockaddr_in
的变量remote强转成结构体struct sockaddr类型指针,这里指针只能看到有限的范围里的值,不需要给它看到的就被自然的屏蔽掉了。我们举些列子来说明一下:
struct A {int name;int age;int ho;
};struct B {int name;int age;
}int main(int agrc, char*argv[]){struct A a = { 11, 22, 33 };struct B *b = (struct B *)&a;...return 0;
}
在上述列子中,b指针指向变量a所在的区域,只是b能看到的范围是受自己的成员大小限制的,如果结构体B的成员的大小能够大于或等于结构体A的大小,那么a变量的内存空间,b指针都能够看到。比如上述例子中,structure A的大小是3个int大小,structure B的大小是2个int的大小,那么structure B只能看到a变量的前两个int,第三个是没有办法看到。与结构体里的成员名称是没有关系的。也就是哪怕structure B定义成下面:
struct B {int aaa;int bbb;
}
b->aaa访问的仍然是a的name,b->bbb访问的仍然是a的age。另外,还有一个特点,假设:
struct B {long aaa;long bbb;
}
struct B的成员类型变成了long,它是8个字节的长度,也就是说b->aaa就访问了a变量的name和age变量,把name的4个字节和age的4个字节都读出来了,有时你不能确定name的4个字节是在age的4个字节前,还是后面, 因为这里还有大小端的问题。
3.用socket向服务端发送信息
int socket_send(int sock, char*req,short len){int ret = -1;struct timeval tv;tv.tv_sec = 20;tv.tv_usec = 0;if(setsockopt(sock,SOL_SOCKET,SO_SNDTIMEO,(char *)&tv,sizeof(tv)) < 0 ){printf("Failed to set Send Timeout\n");return -1;}ret = send(sock,req,len,0);return ret;
}
typedef /* ... */ time_t; // 秒,整型typedef /* ... */ suseconds_t; // 毫秒,有符号整型typedef /* ... */ useconds_t; // 毫秒,无符号整型struct timeval {time_t tv_sec; /* Seconds */suseconds_t tv_usec; /* Microseconds */};
int setsockopt(int sockfd, int level, int optname, const void optval[.optlen], socklen_t optlen);
这个函数可以用来控制socket的一些行为,具体是哪些行为和怎么控制,是由optname,optval来决定的,如设置缓冲区大小等等,level是指定这些行为驻留的协议级别。本例中,我们用这个函数设置发送超时,具体参数描述如下:
- sockfd这个是socket对应的文件描述符
- level,这个是指定操作选项所在的级别,并且必须指定该选项的名称。在socket API级别操作选项,指定的级别就是SOL_SOCKET。对于其他级别的操作选项,需要提供控制这个选项的协议的协议号。如,某个选项是被TCP协议解释的,那么级别应该设置为IPPROTO_TCP(定义在netinet/in.h中)。
- optname:optname和任何指定的选项不经解释地传递给用于解释的适当协议模块。sys/socket.h定义了socket级别的一些选项。本例中,SO_SNDTIMEO就是超时的选项。
- optval,optlen:是用于提供选项值的访问的,在本例中,就是用来访问设定的超时值。
接下来我们就要利用send函数向服务端的socket发送信息,send函数的原型如下:
ssize_t send(int sockfd, const void buf[.len], size_t len, int flags);
- sockfd创建好的socket的文件描述符
- buf 这是要发送信息的缓冲区,发送的内容就放在里面
- len缓冲区中要发送内容的长度
- flags 对于一个已经连接的socket来说,这里指定成NULL或0即可。
接收服务端socket发送过来的消息
int socket_receive(int sock, char*resp,short len){int ret = -1;struct timeval tv;tv.tv_sec = 20;tv.tv_usec = 0;//设置接收超时if(setsockopt(sock,SOL_SOCKET,SO_RCVTIMEO,(char*)&tv,sizeof(tv)) < 0){printf("Failed to set receive timeout\n");return -1;}ret = recv(sock,resp,len,0);printf("Response %s\n",resp);return ret;
}
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
用这个系统调用来接收消息
- sockfd创建好的socket的文件描述符
- buf接收消息的缓冲区
- len缓冲区大小
- flags 对于一个已经连接的socket来说,这里指定成NULL或0即可。
4.客户端socket的main函数:
int main(int argc,char*argv[]){int sock;int read_size;char send_to_server[100] = {0};char recv_from_server[100] = {0};//创建socketsock = create_socket();if(sock == -1){printf("Could not create a socket\n");return 1;}printf("Socket is created\n");//将客户端socket连接到服务端socketif(connect_socket(sock) < 0){printf("Connect to server socket failed\n");return 1;}printf("Successfully connected to server\n");printf("Enter a message: \n");//等待用户输入要发送到服务端socket的消息fgets(send_to_server,100,stdin);//用客户端socket发送消息到服务端socketsocket_send(sock,send_to_server,strlen(send_to_server));//客户端socket收到服务端socket的响应read_size = socket_receive(sock,recv_from_server,100);printf("Server response: %s\n",recv_from_server);//关闭客户端socketclose(sock);shutdown(sock,0);//不再读shutdown(sock,1);//不再写shutdown(sock,2);//不再读写return 0;
}
int shutdown(int socket, int how);
参与
- socket参数是创建好的socket的文件描述符
- how参数指定关闭的类型,SHUT_RD(关闭接收),SHUT_WR(关闭发送),SHUT_RDWR(关闭接收和发送操作)
关于close和shutdown的区别,可以参考这里
服务端socket
1.创建socket
short create_socket(void){short sock;printf("Create a server socket\n");sock = socket(AF_INET,SOCK_STREAM,0);return sock;
}
2.绑定socket
int bind_created_socket(int sock){int ret = -1;int port = 60000;struct sockaddr_in remote = {0};remote.sin_addr.s_addr = htonl(INADDR_ANY);remote.sin_port = htons(port);ret = bind(sock,(struct sockaddr *)&remote,sizeof(remote));return ret;
}
参考
- INADDR_ANY (0.0.0.0)表示socket绑定到任何地址
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
当我们用socket()这个函数创建了一个socket后,这个socket就存在于一个命名空间中,但是没有任何地址分配给它。bind()这个函数要将addr指定的地址分配给这个socket。
- sockfd 参数:socket文件描述符
- addr:将要绑定到socket的地址
- addrlen:地址长度(单位是字节)
3. 服务器端socket的main函数
int main(int argc,char*argv[]){int sock_server;int sock;int client_len;int read_size;struct sockaddr_in server;struct sockaddr_in client;char client_message[100] = {0};char message[100] = {0};const char *msg = "Hello!\n";sock_server = create_socket();if(sock_server == -1){printf("Could not create a socket\n");return 1;}printf("Successfully created a socket\n");if(bind_created_socket(sock_server) < 0){perror("Bind failed\n");return 1;}printf("Bind done\n");listen(sock_server,3);while(1){printf("Waiting for incoming connections...\n");client_len = sizeof(struct sockaddr_in);sock = accept(sock_server,(struct sockaddr *)&client,(socklen_t*)&client_len);if(sock < 0){perror("accept failed\n");return 1;}printf("Connection accepted\n");memset(client_message, '\0',sizeof client_message);memset(message, '\0', sizeof message);if(recv(sock,client_message,100,0) < 0){printf("recv failed");break;}printf("Client response: %s\n",client_message);if(strcmp(msg,client_message) == 0){strcpy(message,"Hi there!");} else {strcpy(message,"Invalid Message!");}if(send(sock,message,strlen(message),0) <0){printf("Send failed\n");return 1;}close(sock);sleep(1);}
int listen(int sockfd, int backlog);
这个函数会将socket标记为一个被动的socket,也就是说,作为一个socket,它会在accpt函数中被用于接受传入的连接请求。backlog是指定等待连接请求的队列大小。比如,本例设置了3,那么也就是队列中等待连接请求处理的数量最大只能有3个,有一个客户端socket请求连接,如果此时队列已满,也就是已有3个在队列中,那么这个新的请求就不会被处理,对应的客户端socket就会收到一个错误,如ECONNREFUSED。
int accept(int sockfd, struct sockaddr *_Nullable restrict addr, socklen_t *_Nullable restrict addrlen);
这个函数会从等待连接的队列中为监听的socket(就是listen()函数参数中那个传入的socket)取出第一个连接请求来创建一个新的socket,返回的值就是新的socket的文件描述符,
- sockfd 就是socket()函数创建的socket文件描述符,这个socket通过bind()函数绑定了一个本地地址,并在listen()函数参数传入,被标记为被动的socket,监听连接。
- addr参数是一个指向sockaddr结构体的指针,这个结构体会用对等端发起连接请求的socket的地址来直充,如果addr没有被填充,即addr是NULL,这种情况下,addrlen也不会被使用,也是NULL。
- addrlen是addr结构体的长度,如果addr是NULL,那么这个addrlen也是NULL。
void *memset(void s[.n], int c, size_t n);
这个函数用常数字节c填充s指针指向的内存区域的前n个字节。