Linux下C语言Socket编程

1 篇文章 0 订阅

(前期使用Ubuntu18.04,后期换成了Deepin20,但是二者都是Debian系的所以各种操作不耽误)

  • 什么是Socket

英语socket是插座,插孔的意思,中文译作套接字;

插销和插座插在一起,就能通电,引申得到两个socket套接在一起就可以通信;

既然是插座和插销,所以socket一定是成对出现的;

IP地址:在网络环境中唯一标识一台主机;

Port:在主机中唯一标识一个进程;

IP地址+Port:在网络环境中唯一标识一个进程,这个进程就是Socket;

Socket是Linux的一种文件类型(伪文件,不会占用实际的存储空间;Linux七种文件类型:占存储空间:普通文件、目录、软连接,不占存储空间:字符设备、块设备、管道、套接字);

Socket是全双工的,一个文件描述符对应两个缓冲区,分别进行读和写:

  • 网络字节序转化

数据在网络中传输就要转化成二进制形式;

主机字节序是小端法,高位存在高地址,低位存在低地址;网络字节序是大端法,高位存在低地址,低位存在高地址:(int类型四个字节,每个字节八位,而一个十六进制数四位,所以一个字节存两个十六进制数)

网络字节序和主机字节序转化的函数:

        uint32_t    htonl(uint32_t    hostlong)//uint32_t:32位无符号整型;htonl:host to network long,32位数值从主机字节序转为网络字节序;主机发送数据到网络环境时用;

        uint16_t    htons(uint16_t    hostshort)//htons:host to network short,16位数值从主机字节序转为网络字节序;

        uint32_t    ntohl(uint32_t    netlong)//32位数值从网络字节序转为主机字节序;主机从网络环境接收数据时用;

        uint16_t    ntohl(uint16_t    netshort)

  • 专门用于IP地址转化的函数

#include <arpa/inet.h>

int  inet_pton(int  af,const  char  *src,void  *dst)//将IP地址由点分十进制转为网络字节序;af是地址族,有两个值AF_INET表示IPv4,AF_INET6表示IPv6;src是待转换的点分十进制IP字符串,const表示只读;dst是转换后的IP;

int  inet_ntop(int  af,const  void  *src,char  *dst,socklen_t  size)//将IP地址由网络字节序转为点分十进制字符串;af地址族;src待转换的网络字节序IP;dst转换后的点分十进制字符串(首地址);size是点分十进制字符串长度;

htonl等函数操作的是数值类型,inet_pton操作的是字符串;

  • sockaddr数据结构

最初的struct  sockaddr是一个描述IPv4的结构体,包括地址类型(16bit)和地址数据(32Byte);

后来经过改进,改为struct  sockaddr_in,包括地址类型=AF_INET(16bit)、端口号(16bit)、IP地址(32bit)和填充(8bit),大小没变,只是成员细分了;

struct  sockaddr_in成员:

        成员1是协议族,成员2是端口号,都是typedef的数据类型;

        成员3是IP地址,又是一个结构体,只有一个成员:

现在只有struct  sockaddr_in,struct  sockaddr已经废弃了,但是现在的函数传参还是struct  sockaddr类型,比如bind()函数有一参struct  sockaddr  *addr,现在只能先定义struct  sockaddr_in  addr,再传参(struct  sockaddr  *)&addr,因此在程序中看到这种强制类型转换要知道是什么意思,这是历史遗留问题,只能通过强转解决;

  • socket()函数

参数int  type:SOCK_STREAM表示“流式”传输,SOCK_DGRAM表示“报式”传输;

参数int  protocol:传0表示使用该type的默认协议,流式默认协议是TCP,报式默认协议是UDP;

一般参数int  domain可取两个值AF_INET和AF_INET6,参数int  type可取两个值SOCK_STREAM和SOCK_DGRAM,参数int  protocol取0;

返回值:成功,返回新创建的socket文件的文件描述符;失败,返回-1;

PS:什么是文件描述符:文件IO操作中,调用open()函数会得到一个结构体,其中包含了操作这个文件所需要的所有文件属性,指向这个结构体的指针被保存在当前进程空间中的一个数组里,结构体指针所在的数组下标就是文件描述符(fd);因此文件描述符是个int正整数;fopen()函数依赖于open()函数,而调用fopen函数会得到一个File *型指针,指向的也是一个结构体,fd是这个结构体的一个成员;

  • bind()函数

  • listen()函数

指定socket可以同时建立多少个连接,如果参数int  backlog为100,并且此时已经建立了100个连接,那么第101个来了就得等着直到前面100个有断开连接的;

操作系统限定最大是128;

  • accept()函数

参数2是个传出参数,只需要定义一个(struct  sockaddr)struct  sockaddr_in  addr,不需要初始化直接取地址传进去,经过函数调用后addr已经完成初始化了,是一个新的socket的sockaddr结构体;返回值也是一个新socket的fd;

作用:服务端调用accept()函数后阻塞,直到收到来自客户端的连接,这时创建一个新的socket负责与客户端通信,而服务端继续阻塞等待下一个客户端的连接;

也就是说accept()函数是只在服务端调用的;

  • connect()函数

用于连接服务器,参数看起来和bind()函数一样,但是这里参数2是初始化好的对方socket的sockaddr结构体、3是结构体大小;

只在客户端调用;

  • 服务端编程
vim  server.c
#include <stdio.h>
#include <stdlib.h>//包含exit()
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>//包含struct sockaddr_in
#include <ctype.h>//包含toupper()
#include <strings.h>//包含bzero()

#define SERV_IP INADDR_ANY  //INADDR_ANY是192.168.x.x内部地址
#define SERV_PORT 8888

int main(void)
{
	int serv_fd, clie_fd;
	struct sockaddr_in serv_addr, clie_addr;
	socklen_t clie_addr_len;
	char buf[BUFSIZ], clie_ip_buf[BUFSIZ];//BUFSIZ是内置的宏,专门用来指定buf大小
	int n, i;

	serv_fd = socket(AF_INET, SOCK_STREAM, 0);

	bzero(&serv_addr, sizeof(serv_addr));//类似于memeset(),bzero()是直接初始化为0
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(SERV_PORT);//host to network short转成网络字节序
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);//host to network long转成网络字节序;
	bind(serv_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));

	listen(serv_fd, 128);
	
	clie_addr_len = sizeof(clie_addr);
	clie_fd = accept(serv_fd, (struct sockaddr *)&clie_addr, &clie_addr_len);//参数3是传入传出参数,因为传入,所以初始化,因为传出,所以取地址
	printf("client IP: %s,  client port: %d\n", inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_ip_buf, sizeof(clie_ip_buf)), ntohs(clie_addr.sin_port));
	while(1){	
		n = read(clie_fd, buf, sizeof(buf));//从客户socket里读,读到buf里,读的空间是sizeof(buf)这么大,读到了n个字符
		for(i = 0; i < n; i++)
			buf[i] = toupper(buf[i]);
		write(clie_fd, buf, n);//往clie_fd里写,写的是buf里的东西,写n个字符;
	}

	close(serv_fd);
	close(clie_fd);

	return 0;
}
:wq
gcc server.c -o server
./server
  • 客户端编程
vim client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>//包含struct sockaddr_in
#include <string.h>//包含memset()

#define SERV_IP "127.0.0.1"
#define SERV_PORT 8888

int main(void)
{
	int clie_fd;
	struct sockaddr_in serv_addr, clie_addr;
	char buf[BUFSIZ];//BUFSIZ是内置的宏,专门用来指定buf大小
	int n;

	clie_fd = socket(AF_INET, SOCK_STREAM, 0);

	memset(&serv_addr, 0, sizeof(serv_addr));//把这一块内存空间全部清成0;memset()用来为新申请的内存做初始化
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(SERV_PORT);//host to network short转成网络字节序
	inet_pton(clie_fd, SERV_IP, &serv_addr.sin_addr.s_addr);//IP字符串转成网络字节序;
	
	connect(clie_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));

	while(1){
		fgets(buf, sizeof(buf), stdin);//参数1:目的缓冲区指针,参数2:缓冲区大小,参数3:源数据流;stdin:标准输入
		write(clie_fd, buf, strlen(buf));//键盘输入hello/n,fgets后buf里的是hello/n/0,strlen(buf)取的是/0之前的长度
		n = read(clie_fd, buf, sizeof(buf));//写完之后发给服务端了,服务端会响应回来,现在该读了
		write(STDOUT_FILENO, buf, n);//把读到的服务器的响应写到屏幕
	}

	close(clie_fd);

	return 0;
}
:wq
gcc client.c -o client
./client

 客户端发送小写字符串,服务端返回大写字符串;

  • 阻塞

程序运行后并不会疯狂地无限循环,因为accept()、read()、write()等函数都是阻塞的,只有客户端连接、发送数据才会推动循环的进行;

  • 查看C函数帮助文档:以socket()函数为例:
:!man socket  # 在Vim中
man socket  # 在终端中

  • Socket工作原理

serv_fd通过accept()返回的是一个和客户端相同sockaddr的socket,对它读写就相当于对客户端读写;

两个clie_fd通过相同的IP和port能够通信;

服务端clie_fd调用write(),将内容写到写缓冲区,在网络的作用下,也写到了客户端的读缓冲区,这时客户端的read()函数就解除阻塞并读到内容;客户端clie_fd调用write()也是同理;

一个socket有两个缓冲区,每个缓冲区都是根据管道的特性打造的,即一端只能读,一端只能写;两个管道实现了socket的全双工通信;

上面提到的缓冲区和char  buf [ ]缓冲区是不同的,前者位于内核区,后者位于用户区stack堆,一点关系都没有;

void exit(int status)来自stdlib.h作用是结束当前进程,退出程序;参数是0表示正常退出,不是0都表示异常退出;

  • 如果Ctrl+C先关闭server再关闭client的话,下次再启动client发消息会闪退:

因为在客户端连接的情况下Ctrl+C关闭服务端,会使服务端进程处于TIME_WAIT状态,不会真正的关闭,依然占用着端口号,所以下次启动会出错;如果我们写了错误处理代码,就会报错;

查看网络进程状态:以8888端口号为例:

netstat -apn | grep 8888
  • 为了方便知道出错的原因,要补充错误处理代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>//包含struct sockaddr_in
#include <ctype.h>//包含toupper()
#include <strings.h>//包含bzero()

#define SERV_IP INADDR_ANY  //INADDR_ANY是192.168.x.x内部地址
#define SERV_PORT 8888

int main(void)
{
	int serv_fd, clie_fd;
	struct sockaddr_in serv_addr, clie_addr;
	socklen_t clie_addr_len;
	char buf[BUFSIZ], clie_ip_buf[BUFSIZ];//BUFSIZ是内置的宏,专门用来指定buf大小
	int n, i;

	serv_fd = socket(AF_INET, SOCK_STREAM, 0);
    if(serv_fd == -1)
    {
        perror("socket error");
        exit(1);
    }

	bzero(&serv_addr, sizeof(serv_addr));//类似于memeset(),bzero()是直接初始化为0
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(SERV_PORT);//host to network short转成网络字节序
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);//host to network long转成网络字节序;
	if(bind(serv_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
    {
        perror("bind error");
        exit(1);
    }

	if(listen(serv_fd, 128) == -1)
    {
        perror("listen error");
        exit(1);
    }
	
	clie_addr_len = sizeof(clie_addr);
	clie_fd = accept(serv_fd, (struct sockaddr *)&clie_addr, &clie_addr_len);//参数3是传入传出参数,因为传入,所以初始化,因为传出,所以取地址
    if(clie_fd == -1)
    {
        perror("accept error");
        exit(1);
    }
	printf("client IP: %s,  client port: %d\n", inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_ip_buf, sizeof(clie_ip_buf)), ntohs(clie_addr.sin_port));
	while(1){	
		n = read(clie_fd, buf, sizeof(buf));//从客户socket里读,读到buf里,读的空间是sizeof(buf)这么大,读到了n个字符
		for(i = 0; i < n; i++)
			buf[i] = toupper(buf[i]);
		write(clie_fd, buf, n);//往clie_fd里写,写的是buf里的东西,写n个字符;
	}

	close(serv_fd);
	close(clie_fd);

	return 0;
}
  • 添加错误处理使代码冗杂,改用自定义函数、封装容错模块、多文件联合编译:

wrap.c(包含了所有自定义容错函数):

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <errno.h>

void perr_exit(const char *s)
{
	perror(s);
	exit(-1);
}

int Socket(int domain, int type, int protocol)
{
	int fd;
	if((fd = socket(domain, type, protocol)) < 0)
		perr_exit("socket error");
	return fd;
}

int Accept(int fd, struct sockaddr *addr, socklen_t *len){
    int new_fd;
again:
    if((new_fd = accept(fd, addr, len)) < 0)
    {
        if((errno == ECONNABORTED) || (errno == EINTR))
            goto again;
        else
            perr_exit("accept error");
    }
    return new_fd;    
}

int Bind(int fd, const struct sockaddr *addr, socklen_t len)
{
    int result;
    if((result = bind(fd, addr, len)) < 0)
        perr_exit("bind error");
    return result;
}

int Connect(int fd, const struct sockaddr *addr, socklen_t len)
{
    int result;
    if((result = connect(fd, addr, len)) < 0)
        perr_exit("connect error");
    return result;
}

int Listen(int fd, int backlog)
{
    int result;
    if((result = listen(fd, backlog)) < 0)
        perr_exit("listen error");
    return result;
}

/*
size_t nbytes   读的字节数(范围); 
ssize_t n       实际读到的字节数;
*/
ssize_t Read(int fd, void *ptr, size_t nbytes)
{
    ssize_t n;
again:
    if((n = read(fd, ptr, nbytes)) == -1)
    {
        if(errno == EINTR)
            goto again;
        else
            perr_exit("read error");
    }
    return n;
}

ssize_t Write(int fd, const void *ptr, size_t nbytes)
{
    ssize_t n;
again:
    if((n = write(fd, ptr, nbytes)) == -1)
    {
        if(errno == EINTR)
            goto again;
        else
            return -1;
    }
    return n;
}

int Close(int fd)
{
    int n;
    if((n = close(fd)) == -1)
        perr_exit("close error");
    return n;
}

//参数3:应该读取的字节数
ssize_t Readn(int fd, void *vptr, size_t n)
{
    size_t nleft;   //unsigned int 剩余未读字节数
    ssize_t nread;  //(signed) int 读一次实际读到的字节数
    char *ptr;
    
    ptr = vptr;
    nleft = n;

    while(nleft > 0)
    {
        if((nread = read(fd, ptr, nleft)) < 0)
        {
            if(errno == EINTR)
                nread = 0;
            else
                return -1;                            
        }
        else if(nread == 0)
            break;
        nleft -= nread;
        ptr += nread;
    }
    return n - nleft;
}

ssize_t Writen(int fd, const void *vptr, size_t n)
{
    size_t nleft;
    ssize_t nwritten;
    const char *ptr;

    ptr = vptr;
    nleft = n;
    while(nleft > 0)
    {
        if((nwritten = write(fd, ptr, nleft)) <= 0)
        {
            if(nwritten < 0 && errno == EINTR)
                nwritten = 0;
            else 
                return -1;
        }  
        nleft -= nwritten;
        ptr += nwritten;  
    }    
    return n;
}

static ssize_t my_read(int fd, char *ptr)
{
    static int read_cnt;
    static char *read_ptr;
    static char read_buf[100];

    if(read_cnt <= 0)
    {
again:
        if((read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0)
        {
            if(errno = EINTR)    
                goto again;
            return -1;
        }else if(read_cnt == 0);
            return 0;
        read_ptr = read_buf;
    }
    read_cnt--;
    *ptr = *read_ptr++;
    return 1;
}

/*
readline --- fgets
传出参数vptr
*/
ssize_t Readline(int fd, void *vptr, size_t maxlen)
{
    ssize_t n, rc;
    char c, *ptr;
    
    ptr = vptr;
    
    for(n = 1; n < maxlen; n ++)
    {
        if((rc = my_read(fd, &c)) == 1)
        {
            *ptr++ = c;
            if(c == '\n')
                break;
        }else if(rc == 0)
        {
            *ptr = 0;
            return n - 1;
        }else
            return -1;
    }
    *ptr = 0;
    return n;
}

int Fork()
{
    int pid;
    if((pid = fork()) < 0)
        perr_exit("fork error");
    return pid;
}

wrap.h:

#ifndef __WRAP_H_
#define __WRAP_H_

void perr_exit(const char *s);
int Socket(int domain, int type, int protocol);
int Accept(int fd, struct sockaddr *addr, socklen_t *len);
int Bind(int fd, const struct sockaddr *addr, socklen_t len);
int Connect(int fd, const struct sockaddr *addr, socklen_t len);
int Listen(int fd, int backlog);
ssize_t Read(int fd, void *ptr, size_t nbytes);
ssize_t Write(int fd, const void *ptr, size_t nbytes);
int Close(int fd);
ssize_t Readn(int fd, void *vptr, size_t n);
ssize_t Writen(int fd, const void *vptr, size_t n);
static ssize_t my_read(int fd, char *ptr);
ssize_t Readline(int fd, void *ptr, size_t maxlen);
int Fork();

#endif

server.c:

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

#include "wrap.h"//包含自定义的函数

#define SERV_IP INADDR_ANY
#define SERV_PORT 8888

int main(void)
{
	int serv_fd, clie_fd;
	struct sockaddr_in serv_addr, clie_addr;
	socklen_t clie_addr_len;
	char buf[BUFSIZ], clie_ip_buf[BUFSIZ];
	int n, i;

	serv_fd = Socket(AF_INET, SOCK_STREAM, 0);//用自定义的Socket()

	bzero(&serv_addr, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(SERV_PORT);
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	Bind(serv_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));

	Listen(serv_fd, 128);
	
	clie_addr_len = sizeof(clie_addr);
	clie_fd = Accept(serv_fd, (struct sockaddr *)&clie_addr, &clie_addr_len);//用自定义的Accept()
	printf("client IP: %s,  client port: %d\n", inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_ip_buf, sizeof(clie_ip_buf)), ntohs(clie_addr.sin_port));
	while(1){	
		n = Read(clie_fd, buf, sizeof(buf));
		for(i = 0; i < n; i++)
			buf[i] = toupper(buf[i]);
		Write(clie_fd, buf, n);
	}

	Close(serv_fd);
	Close(clie_fd);

	return 0;
}

 client.c同样换用自定义的函数;

联合编译并执行:

为什么不直接:

因为联合编译时,如果修改其中一个.c文件,另一个未修改的不需要gcc -c,分开编译可以只编译修改的,在工程特别大的时候可以节省时间;

  • 网络编程涉及的errno和错误处理

accept():

EINTER(Error INTERrupted):这个系统调用(system call)让一个被捕捉(caught)的信号(signal)打断了(interrupted);

ECONNABORTED(Error CONNection ABORTED):连接(connection)异常断开了(aborted);

以上两个ERROR都是在程序阻塞的时候被打断了,处理方式是要么重启即goto again,要么退出;connect()函数不会遇到这种ERROR,因为connect()是客户端主动发起的,不阻塞;

read():

返回值:

1、> 0:可能==sizeof(buf)也可能<sizeof(buf);

2、==0:数据读完,读到文件/管道/Socket的末尾,对面Socket写端关闭;

3、== -1:异常

        1)errno == EINTR:被信号中断,此时重启或退出;

        2)errno == EAGAIN或EWOULDBLOCK:非阻塞方式读,并且没有数据;

        3)其他:出现错误,perr_exit();

  • read()、write()函数的参数size_t、返回值ssize_t

size_t是自定义的描述size的无符号整型,应该读/写的范围;ssize_t是有符号整型,实际读/写的范围;

  • 自己封装的Readn()函数什么时候用

从Socket里读数据,Socket的数据是从网络中接收到的,而以太网帧最大1500字节,如果我的需求是读4096字节,那么我用read()函数最多读1500字节就返回了,而我们要求读4096字节然后返回,因此自己封装一个Readn(),即通过重复调用read()实现读n个字节;

  • 三次握手和四次握手

1、网络层是不稳定的,容易受硬件影响(现在随着技术进步,影响已经很小了),比如有路由器宕机就会影响网络层质量,也就是说网络层有丢包的风险;

2、由于网络层的不稳定性,传输层也分两类:

        1)完全不弥补:顺应网络层的特点,以UDP为代表,面向无连接的报文传输,电报发报文的特点就是接收无应答,丢包不重传;

        2)完全弥补:弥补网络层的缺陷,以TCP为代表,面向连接的数据包传输, 接收有应答,丢包会重传;

3、TCP既然是面向连接的,就要先建立连接再发数据,如果由于网络层的不稳定性导致连接断开,就得重新建立连接,发送完数据还要关闭连接;

4、上述建立连接的过程就是三次握手:

        1)Client----SYN 1000(0)---->Server

              客户端向服务端发起请求:包号是1000,字节数是0(即携带数据的长度是0字节);

        2)Client<----SYN 8000(0)ACK 1001----Server

              服务端响应客户端并向客户端发起请求:响应:确认收到1001之前的,下次发1001;请求:包号是8000,字节数是0;

        3)Client----ACK 8001---->Server

              客户端响应服务器:8001之前的包收到,从8001开始发吧;

5、三次握手完成,接下来就是发数据(不一定非得一来一回):

        1)Client----1001(20)ACK 8001---->Server

              客户端确响应务端并向服务端发数据:响应:确认收到8001之前的,下次发8001;发数据:包号是1001,长度是20(即1001~1020);

        2)Client<----8001(40)ACK 1021----Server

              服务端响应客户端并向客户端发数据:响应:确认收到1021之前的,下次发1021;发数据:包号是8001,长度是40(即8001~8040);

        3)Client----ACK 8041----Server

              客户端响应服务端:确认收到8041之前的,下次发8041;

6、发完之后关闭连接的过程就是四次握手:

        为什么有4次:因为允许半关闭状态;什么是半关闭状态:一端关闭另一端不关闭,客户端请求关闭并收到服务端的确认后,客户端能接收并确认服务端的数据,但不再给服务端发数据;

        1)Client----FIN 1021(0)ACK 8041---->Server

              客户端请求关闭:包号是1021,字节数是0;并且确认8041;

        2)Client<----ACK 1022----Server

              服务端确认收到1021;此时客户端关闭,不再发数据,但能接收数据并发确认包;

        3)Client<----FIN 8041(0)ACK 1022----Server

              服务端请求关闭,包号是8041,字节数是0;并且确认1022;

        4)Client----ACK 8042---->Server

              客户端确认收到8042;此时服务端关闭,不再发数据;

        注意从三次握手、发送数据到四次握手,请求序列号和确认序列号都是连贯的;

  • 多进程Server
#include <stdio.h>
#include <stdlib.h> //exit()
#include <unistd.h> //close()
#include <arpa/inet.h> //网络编程有关函数
#include <strings.h> //bzero()
#include <ctype.h> //toupper()
#include <sys/wait.h> //signol()、WNOHANG
#include <signal.h> //SIGCHLD
#include "wrap.h" //自定义函数们

#define SERV_IP "127.0.0.1"
#define SERV_PORT 8888

//用来处僵尸进程的
void wait_child(int signo)
{
    while(waitpid(0, NULL, WNOHANG) > 0)
    return;
}

int main(void)
{
	int lfd, cfd;
	struct sockaddr_in serv_addr, clie_addr;
	socklen_t clie_addr_len;
	pid_t pid;
	char buf[BUFSIZ], clie_ip[BUFSIZ];
	int n, i;
	
	lfd = Socket(AF_INET, SOCK_STREAM, 0);

	bzero(&serv_addr, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	inet_pton(AF_INET, SERV_IP, &serv_addr.sin_addr.s_addr);
	serv_addr.sin_port = htons(SERV_PORT);
	Bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));

	Listen(lfd, 128);

	//无限循环,每连接一个客户端,就创建一个cfd与之连接,并创建一个子进程,产生分支:
	//父进程的cfd没用了,close掉继续等待下一个;
	//子进程的lfd没用了,退出循环去和客户端通信;
	while (1)
	{
		clie_addr_len = sizeof(clie_addr);
		cfd = Accept(lfd, (struct sockaddr *)&clie_addr, &clie_addr_len);
		
		printf("%s:%d\n", inet_ntop(AF_INET, &clie_addr.sin_addr, clie_ip, sizeof(clie_ip)), ntohs(clie_addr.sin_port));

		pid = Fork();
		if(pid == 0)
		{
			close(lfd);
			break;
		}
		else
		{
			close(cfd);
			//用来处理僵尸进程
			//僵尸进程是子进程死了没有被父进程回收,白白占据了资源
			signal(SIGCHLD, wait_child);
		}
	}
	if(pid == 0)
	{
		while(1)
			{
				n = Read(cfd, buf, sizeof(buf));
				if(n == 0)//client closed
				{
					close(cfd);
					return 0;
				}
				else
				{
					for(i = 0; i < n; i ++)
						buf[i] = toupper(buf[i]);
					Write(cfd, buf, n);
				}
			}
	}
	return 0;
}

 不用signal()的话会有僵尸进程(STAT为Z+的进程):

  • 多线程Server
#include <stdio.h>
#include <unistd.h> //STDOUT_FILENO
#include <string.h>
#include <arpa/inet.h> //网络编程有关函数
#include <ctype.h> //toupper()
#include <pthread.h>
#include "wrap.h" //自定义函数们

#define SERV_IP "127.0.0.1"
#define SERV_PORT 8888
#define MAXLINE 8192

//自定义结构体,将socket的addr和fd合并
struct sock_info
{
	struct sockaddr_in addr;
	int fd;
};

void *process(void *arg)
{
	int n, i;
	struct sock_info *ts = (struct sock_info *)arg;
	char buf[MAXLINE];
	char str[INET_ADDRSTRLEN];

	while(1)
	{
		n = Read(ts->fd, buf, MAXLINE);
		if(n == 0)
		{
			printf("client is closed...\n");
			break;
		}
		printf("%s:%d\n", inet_ntop(AF_INET, &ts->addr.sin_addr, str, sizeof(str)), ntohs(ts->addr.sin_port));
		Write(STDOUT_FILENO, buf, n);
		for(i = 0; i < n; i ++)
			buf[i] = toupper(buf[i]);
		Write(ts->fd, buf, n);
	}
	close(ts->fd);
	return (void *)0;
}

int main(void)
{
	int listen_fd, connect_fd;
	struct sockaddr_in serv_addr, clie_addr;
	socklen_t clie_addr_len;
	struct sock_info ts[256];
	pthread_t tid;
	int i = 0;
	
	listen_fd = Socket(AF_INET, SOCK_STREAM, 0);

	bzero(&serv_addr, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	inet_pton(AF_INET, SERV_IP, &serv_addr.sin_addr.s_addr);
	serv_addr.sin_port = htons(SERV_PORT);
	Bind(listen_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));

	Listen(listen_fd, 128);

	printf("Accepting...\n");

	//无限循环,主线程负责监听,收到连接后创建子线程与之通信
	while (1)
	{
		clie_addr_len = sizeof(clie_addr);
		connect_fd = Accept(listen_fd, (struct sockaddr *)&clie_addr, &clie_addr_len);

		ts[i].addr = clie_addr;
		ts[i].fd = connect_fd;

		//创建子线程
		pthread_create(&tid, NULL, process, (void *)&ts[i]);
		//子线程设置为分离状态,防止僵尸线程
		pthread_detach(tid);
		i ++;
	}
	return 0;
}

引用pthread.h时不能直接链接:

因为pthread不是Linux默认的库,加个参数就可以了:

  • TO BE CONTINUED. . .

 

  • 6
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值