网络套接字socket编程之TCP

时间:2022-04-10 10:21:22

概述

在刚开始学习网络套接字编程时,接触到了socket这个词。我们在以前学到Linux的时候了解到Linux下一切皆文件,并大致分为普通文件、目录文件、连接文件、设备和设备文件、管道。而套接字呢是用来实现网上的进程间的通信的,所以套接字也是文件。在TCP/IP协议中,IP地址和端口号唯一标识网络中的一个唯一进程,IP地址和端口号就是套接字。

网络字节序

网络中要实现通信,少不了数据的传输。所以这里就引入了网络字节序的概念。
在前边的学习中,我们接触到大端和小端的概念。小端:数据的地位在低地址,高位在高地址;大端:数据的低位在高地址,高位在低地址。网络数据流同样也有大端和小端之分。网络数据流先发出的是低地址,后发出的是高地址。TCP/IP规定,网络数据流采用大端字节序,即就是低位在高地址。我们之所以会说到大端和小端?是因为,网络通信的时候必须知道端口号,如果发送端是大端字节序,接收端是小端字节序,那么最后看到的端口号就是不正确的端口号,所以,我们必须将端口号在发送端和接收端之间转换成统一的字节序形式。

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换

网络套接字socket编程之TCP

这些函数名很好记,h表示host(主机),n表示network(网络),l表示32位长整数(即long类型),s表示16位短整数(即short类型)。例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

socket地址的数据结构类型

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6。然而,各种网络协议的地址格式并不相同,如下图示:

网络套接字socket编程之TCP

IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表中,包括16位端口号和32位IP地址。

socket编程的相关函数

套接字创建函数socket

网络套接字socket编程之TCP

这个函数就是用来创建套接字的。

其中,domain是表示建立的socket类型,参数如下:

网络套接字socket编程之TCP

至于type则是表示的是数据报传输还是字节流传输,参数如下:

网络套接字socket编程之TCP

最后呢protocol则表示创建的方式,一般缺省为0。

服务器绑定函数bind

网络套接字socket编程之TCP

这个函数用来绑定服务器的ip地址与端口号。

其中,参数sockfd表示的是服务器的套接字——也就是socket函数的返回结果。

而参数addr表示的是socket服务器的地址内容,结构体内的变量则去填写ip地址与端口号。

而sockaddr_in这个结构体成员则是如下所示:

网络套接字socket编程之TCP

这里呢我需要介绍一些对IP地址进行操作的函数:

网络套接字socket编程之TCP

其中呢我主要介绍一下inet_addr和inet_ntoa这两个,前者是将字符串表示的ip地址转成点分十进制表示的ip地址,而后者则是将一个点分十进制的ip地址转成一个字符串。

至于最后的addrlen则表示的是传入的addr的长度,可用sizeof得到。

设置监听状态函数listen

网络套接字socket编程之TCP

这个函数是用来设置sockfd套接字为监听状态的,用来监听客户端的连接。

其中sockfd表示要被设置的套接字,而backlog表示的是服务器链接达到最大的数量之后,还可以放到等待队列的链接个数,所以一般不要设太大。

请求连接函数connect

一般呢这个函数用于客户端,用来请求对服务器的连接。

网络套接字socket编程之TCP

其中呢参数sockfd表示的表示的是要链接到服务器的客户端套接字;参数addr表示的是服务器的地址与端口号;参数addrlen表示的是addr的大小一般使用sizeof得到。

单进程的套接字TCP通信

服务器:

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

static void usage(const char* proc)
{
printf("Usage: %s [local_ip] [local_port]\n", proc);
}

int startup(const char* _ip, int _port)//建立监听套接字
{
int sock = socket(AF_INET, SOCK_STREAM, 0);//创建一个socket
if(sock < 0){
perror("socket");
exit(2);
}
//为网络协议地址赋值
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = inet_addr(_ip);

if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){//绑定ip与端口到套接字上面
perror("bind");
exit(3);
}

if(listen(sock, 5)){//设置监听套接字,5表示监听的最大个数
perror("listen");
exit(4);
}
return sock;
}

int main(int argc, char* argv[])
{
if(argc != 3){
usage(argv[0]);
exit(1);
}

int server_sock = startup(argv[1],atoi(argv[2]));

while(1){
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &len);//获取连接并保存连接到的客户端地址
if(client_sock < 0){
perror("accept");
continue;
}

printf("IP is %s, Port is %d\n", inet_ntoa(client_addr.sin_addr), htons(client_addr.sin_port));//打印连接到的ip地址和端口号

char buf[1024];
while(1){
int ret = read(client_sock, buf, sizeof(buf)-1);//服务器是先读后写,fd表示的是收到的客户端文件描述符
if(ret < 0){
perror("read");
exit(5);
}else if(ret == 0){
printf("client quit\n");
close(client_sock);
break;
}else{
buf[ret] = 0;
printf("client #:%s\n", buf);
write(client_sock, buf, strlen(buf)); //读完之后将读到的数据再次返回写给客户端  
}
}
close(client_sock);
}
close(server_sock);
return 0;
}
客户端:

#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<string.h>
#include<stdlib.h>
#include<netinet/in.h>

void usage(const char* proc)
{
printf("Usage: %s [local_ip] [local_port]\n", proc);
}

int main(int argc, char* argv[])
{
if(argc != 3){
usage(argv[0]);
exit(1);
}
//客户端不用绑定ip与端口号,因为是多对一的
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0){
perror("socket");
exit(2);
}

struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(atoi(argv[2]));
local.sin_addr.s_addr = inet_addr(argv[1]);
if(connect(sock, (struct sockaddr*)&local, sizeof(local)) < 0){用connect函数请求连接到服务器
perror("connect");
exit(3);
}

printf("connect success!\n");

char buf[1024];
while(1){
printf("client #:");//开始通信,客户端是先写后读
fflush(stdout);

int ret = read(0, buf, sizeof(buf)-1);//从标准输入中读取数据
if(ret <= 0){
perror("read");
exit(4);
}else{
buf[ret-1] = 0;
write(sock, buf, strlen(buf));
}

ret = read(sock, buf, sizeof(buf)-1);//再从套接字中读取数据
if(ret < 0){
perror("read");
exit(5);
}else if(ret == 0){
printf("server quit\n");
break;
}else{
buf[ret] = 0;
printf("server #:%s\n", buf);
}
}
close(sock);
return 0;
}
我们在实现了上面的代码后运行一下:

如果不加上ip地址和端口号,就会报错

网络套接字socket编程之TCP

那么现在加上端口号和ip地址去运行:

网络套接字socket编程之TCP

先运行tcp_server,然后等待tcp_client的运行

网络套接字socket编程之TCP

运行tcp_client,然后就会看到连接成功

网络套接字socket编程之TCP

此时服务器端就会打印出客户端的ip地址和端口号。

然后客户端输入nihao,那么服务器端也会显示nihao。

网络套接字socket编程之TCP

网络套接字socket编程之TCP


多进程的套接字TCP通信

在上面呢我们实现了一个单进程TCP通信,但是呢现实生活中不可能就一个进程进行通信的,而在这之前我们也提到过多进程的概念以及多进程的编写,所以呢在这里我们也实现一下多进程TCP通信。

服务器端:

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

static void usage(const char* proc)
{
printf("Usage: %s [local_ip] [local_port]\n", proc);
}

int startup(const char* _ip, int _port)
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0){
perror("socket");
exit(2);
}
//为网络协议地址赋值
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);//htons就是将host主机转换为net网络中的,而s代表short,l则代表long
local.sin_addr.s_addr = inet_addr(_ip);

if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){
perror("bind");
exit(3);
}

if(listen(sock, 5)){//5表示监听的最大个数
perror("listen");
exit(4);
}
return sock;
}

int main(int argc, char* argv[])
{
if(argc != 3){
usage(argv[0]);
exit(1);
}

int server_sock = startup(argv[1],atoi(argv[2]));

while(1){
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &len);
if(client_sock < 0){
perror("accept");
continue;
}

printf("IP is %s, Port is %d\n", inet_ntoa(client_addr.sin_addr), htons(client_addr.sin_port));

pid_t id = fork();//调用fork实现多进程
if(id == 0){//child
close(server_sock);
if(fork() > 0){

}
char buf[1024];
while(1){
ssize_t s = read(client_sock, buf, sizeof(buf)-1);
if(s > 0){
buf[s]=0;
printf("client #: %s\n", buf);
}else if(s == 0){
printf("client quit\n");
break;
}else{
perror("read");
exit(5);
}
}
close(client_sock);
}else{//father
close(client_sock);
while(waitpid(-1, NULL, WNOHANG));
continue;
}

}
return 0;
}
客户端:

#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<string.h>
#include<stdlib.h>
#include<netinet/in.h>

void usage(const char* proc)
{
printf("Usage: %s [local_ip] [local_port]\n", proc);
}

int main(int argc, char* argv[])
{
if(argc != 3){
usage(argv[0]);
exit(1);
}

int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0){
perror("socket");
exit(2);
}

struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(atoi(argv[2]));
local.sin_addr.s_addr = inet_addr(argv[1]);
if(connect(sock, (struct sockaddr*)&local, sizeof(local)) < 0){
perror("connect");
exit(3);
}

printf("connect success!\n");

char buf[1024];
while(1){
printf("client #:");
fflush(stdout);

int ret = read(0, buf, sizeof(buf)-1);
if(ret <= 0){
perror("read");
exit(4);
}else{
buf[ret-1] = 0;
write(sock, buf, strlen(buf));
}

ret = read(sock, buf, sizeof(buf)-1);
if(ret < 0){
perror("read");
exit(5);
}else if(ret == 0){
printf("server quit\n");
break;
}else{
buf[ret] = 0;
printf("server #:%s\n", buf);
}
}
close(sock);
return 0;
}
多进程的特点就在于调用fork()函数处理了一下,让子进程去走,而父进程一直等待。

服务器端可以创建多个子进程去处理客户端发来的信息。当每次收到一个新的客户端的连接请求的时候,我们就会fork()出一个子进程,父进程用于等待子进程,子进程用于执行 读客户端发的数据 的操作。细心的你可能会发现,我们在子进程读取信息之前还进行了一次fork(),这是为什么呢?其实,我们用子进程fork()出一个孙子进程,终止掉儿子进程,儿子进程被它的父进程回收,此时的孙子进程就是一个孤儿进程,被1号进程领养。这样做的目的就是,不要让儿子进程等待孙子进程太久而消耗太多的系统资源。 

网络套接字socket编程之TCP

另外,这里还涉及到父进程关闭通信套接字,子进程关闭监听套接字。这是因为,父进程是来监听的,不需要通信,子进程是读取信息的,不需要监听。

这里呢运行结果我就不显示了,如果有兴趣你可以自己去试一下。

多线程的套接字TCP通信

上面已经提到了多进程,那么这里我们就不得不提到多线程了。以前的学习中我们知道线程是系统内部的一个执行流,是在进程的地址空间中运行的。而进程是程序的一次动态执行过程,系统中进程过多的话会增加系统的负担,所以就有了多线程的通信。

服务器端:

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

static void usage(const char* proc)
{
printf("Usage: %s [local_ip] [local_port]\n", proc);
}

int startup(const char* _ip, int _port)
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0){
perror("socket");
exit(2);
}

struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);//htons就是将host主机转换为net网络中的,而s代表short,l则代表long
local.sin_addr.s_addr = inet_addr(_ip);

if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){
perror("bind");
exit(3);
}

if(listen(sock, 5)){//5表示监听的最大个数
perror("listen");
exit(4);
}
return sock;
}

void* thread_handler(void* arg)//为每一个线程执行客户端的读写
{
int sock = (int)arg;
printf("sock:%d\n", sock);
char buf[1024];
while(1){
ssize_t s = read(sock, buf,sizeof(buf)-1);
if(s > 0){
buf[s]=0;
printf("client #: %s\n", buf);
if(write(sock, buf, sizeof(buf)-1) < 0){
break;
}
}else if(s == 0){
printf("client quit\n");
break;
}else{
perror("read");
break;
}
}
close(sock);
}

int main(int argc, char* argv[])
{
if(argc != 3){
usage(argv[0]);
exit(1);
}

int server_sock = startup(argv[1],atoi(argv[2]));

while(1){
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &len);
if(client_sock < 0){
perror("accept");
continue;
}

printf("IP is %s, Port is %d\n", inet_ntoa(client_addr.sin_addr), htons(client_addr.sin_port));

pthread_t tid;
int ret = pthread_create(&tid, NULL, thread_handler, (void*)(client_sock));//创建一个新线程来执行客户端的读写操作
if(ret < 0){
perror("pthread_create");
exit(5);
}
pthread_detach(tid);//设置线程状态为分离状态,这样主线程不用去等待,可以继续执行循环
}
close(server_sock);
return 0;
}

客户端:

#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<string.h>
#include<stdlib.h>
#include<netinet/in.h>

void usage(const char* proc)
{
printf("Usage: %s [local_ip] [local_port]\n", proc);
}

int main(int argc, char* argv[])
{
if(argc != 3){
usage(argv[0]);
exit(1);
}

int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0){
perror("socket");
exit(2);
}

struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(atoi(argv[2]));
local.sin_addr.s_addr = inet_addr(argv[1]);
if(connect(sock, (struct sockaddr*)&local, sizeof(local)) < 0){
perror("connect");
exit(3);
}

printf("connect success!\n");

char buf[1024];
while(1){
printf("client #:");
fflush(stdout);

int ret = read(0, buf, sizeof(buf)-1);
if(ret <= 0){
perror("read");
exit(4);
}else{
buf[ret-1] = 0;
write(sock, buf, strlen(buf));
}

ret = read(sock, buf, sizeof(buf)-1);
if(ret < 0){
perror("read");
exit(5);
}else if(ret == 0){
printf("server quit\n");
break;
}else{
buf[ret] = 0;
printf("server #:%s\n", buf);
}
}
close(sock);
return 0;
}

多线程呢就是在主线程中创建出一个新线程,新线程的执行函数是读取信息。类似于上边的多进程间的通信,我们可以将新的线程进行分离,分离之后的线程就不需要主线程去等待,而是由操作系统区回收。(这里我们不可以join新线程,如果这样做的话,主线程还是需要花费很长的时间去等待,所以,新的线程还是由系统去回收)


补充:server bind失败的原因?

这里呢我们来模拟一种情形:先运行server,再运行client,client给server发数据,然后ctrl+c终止调server,立即再次启动server,会出现什么现象呢?

网络套接字socket编程之TCP

网络套接字socket编程之TCP

我们可以看到Address already is in use。

这是什么原因呢?服务器终止程序,服务器就是主动发起断开连接请求的一方,根据TCP的3次握手4次挥手协议(如果有些不清楚请点击这里查看tcp协议的三次握手与四次挥手),主动发起连接断开请求的一方,最后必须等待2MSL的时间确认客户端是否收到自己的确认信息。这里,我们立即运行server的时候,server还是在TIME_WAIT状态,所以bind的时候就会出现地址已经被占用。

解决方法:socket之后,bind之前,加语句
int opt = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
就是为了处理这个问题。

其中的sock就是socket函数创建套接字的返回值。